Skip to content

Commit

Permalink
feat: 카메라 촬영 기능 연결 #514 (#524)
Browse files Browse the repository at this point in the history
* build: androidx camera 의존성 추가

- 버전 카탈로그 활용

* build: AndroidManifest 카메라 접근 기능 수정

- android.hardware.camera.any 로 설정하여 전, 후면 모두 사용 가능하도록 변경

* ui: 사진 업로드 다이얼로그에서 ㄷ사용할 문자열 리소스 설정

- 권한 관련 안내 메시지 추가

* feat: 카메라 기능 추가

- 접근 권한 확인 후 카메라 실행
- 카메라에서 촬영한 사진을 가져와 Activity에게 이미지 URI 전달

* style: ktlint 적용

* fix: 권한 요청 스낵바와 에러 메시지 스낵바 분리

* ui: 권한 요청 스낵바의 문구 수정

* fix: 외부 저장소 쓰기 권한 추가
  • Loading branch information
Junyoung-WON authored Oct 24, 2024
1 parent 51e37e7 commit e911a10
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 74 deletions.
7 changes: 7 additions & 0 deletions android/Staccato_AN/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ dependencies {

// Lottie
implementation(libs.lottie)

// CameraX
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extension)
}

secrets {
Expand Down
7 changes: 4 additions & 3 deletions android/Staccato_AN/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
</intent>
</queries>

<!-- 카메라(추후 기능 추가) -->
<!-- 카메라 -->
<uses-feature
android:name="android.hardware.camera"
android:name="android.hardware.camera.any"
android:required="false" />

<!-- 인터넷 -->
Expand All @@ -24,8 +24,9 @@
<!-- 앨범 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 카메라(추후 기능 추가) -->
<!-- 카메라 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:name=".StaccatoApplication"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.on.staccato.presentation.common

import android.Manifest
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.READ_MEDIA_IMAGES
import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
Expand All @@ -16,32 +19,77 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import com.on.staccato.R
import com.on.staccato.databinding.FragmentPhotoAttachBinding
import com.on.staccato.presentation.staccatocreation.OnUrisSelectedListener
import com.on.staccato.presentation.util.showToast
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

@AndroidEntryPoint
class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler {
private var _binding: FragmentPhotoAttachBinding? = null
private val binding get() = _binding!!

private lateinit var uriSelectedListener: OnUrisSelectedListener
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var requestGalleryPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var requestCameraPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var galleryLauncher: ActivityResultLauncher<Intent>
private lateinit var cameraLauncher: ActivityResultLauncher<Uri>
private var multipleAbleOption: Boolean = false
private var currentImageUri: Uri? = null

override fun onAttach(context: Context) {
super.onAttach(context)
initUrisSelectedListener(context)
initRequestPermissionLauncher()
initCameraLauncher()
initGalleryLauncher()
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentPhotoAttachBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
binding.handler = this
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
currentImageUri = null
}

override fun onCameraClicked() {
checkPermissionsAndLaunch(
permissions = CAMERA_REQUIRED_PERMISSIONS,
requestPermissionLauncherOnNotGranted = requestCameraPermissionLauncher,
onGranted = { startCamera() },
)
}

override fun onGalleryClicked() {
checkPermissionsAndLaunch(
permissions = arrayOf(GALLERY_REQUIRED_PERMISSION),
requestPermissionLauncherOnNotGranted = requestGalleryPermissionLauncher,
onGranted = { launchGallery() },
)
}

fun setMultipleAbleOption(option: Boolean) {
multipleAbleOption = option
}
Expand All @@ -55,16 +103,65 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler {
}

private fun initRequestPermissionLauncher() {
requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isPermissionGranted ->
if (isPermissionGranted) {
launchGallery()
} else {
showPermissionSnackBar()
requestCameraPermissionLauncher = buildRequestPermissionLauncher { startCamera() }
requestGalleryPermissionLauncher = buildRequestPermissionLauncher { launchGallery() }
}

private fun initCameraLauncher() {
cameraLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
val imageUri = currentImageUri
if (isSuccess && imageUri != null) {
uriSelectedListener.onUrisSelected(imageUri)
dismiss()
}
}
}

private fun initGalleryLauncher() {
galleryLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val imageUris = extractImageUris(result.data)
if (imageUris.isNotEmpty()) {
uriSelectedListener.onUrisSelected(*imageUris.toTypedArray())
dismiss()
} else {
showGalleryErrorSnackBar()
}
}
}
}

private fun buildRequestPermissionLauncher(actionOnPermissionGranted: () -> Unit): ActivityResultLauncher<Array<String>> =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.all { (_, isGranted) -> isGranted }) {
actionOnPermissionGranted()
} else {
showPermissionSnackBar()
}
}

private fun startCamera() {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent.resolveActivity(requireActivity().packageManager) != null) {
launchCamera()
} else {
showNoCameraSnackBar()
}
}

private fun launchCamera() {
try {
currentImageUri = createImageUri()
currentImageUri?.let { imageUri ->
cameraLauncher.launch(imageUri)
}
} catch (e: ActivityNotFoundException) {
showCameraErrorSnackBar()
}
}

private fun launchGallery() {
val intent =
Intent(Intent.ACTION_PICK)
Expand All @@ -73,43 +170,73 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler {
galleryLauncher.launch(intent)
}

private fun checkPermissionsAndLaunch(
permissions: Array<String>,
requestPermissionLauncherOnNotGranted: ActivityResultLauncher<Array<String>>,
onGranted: () -> Unit,
) {
val isPermissionGranted =
permissions.all { permission ->
ContextCompat.checkSelfPermission(
requireContext(), permission,
) == PackageManager.PERMISSION_GRANTED
}
if (isPermissionGranted) {
onGranted()
} else {
requestPermissionLauncherOnNotGranted.launch(permissions)
}
}

private fun showPermissionSnackBar() {
val snackBar = makeSnackBar()
setSnackBarAction(snackBar)
showSettingSnackBar(R.string.snack_bar_require_photo_album_permission)
}

private fun showCameraErrorSnackBar() {
showSnackBar(R.string.snack_bar_camera_error)
}

private fun showGalleryErrorSnackBar() {
showSnackBar(R.string.snack_bar_gallery_error)
}

private fun showNoCameraSnackBar() {
showSnackBar(R.string.snack_bar_no_camera)
}

private fun showSnackBar(
@StringRes resId: Int,
) {
val snackBar = makeSnackBar(resId)
snackBar.show()
}

private fun showSettingSnackBar(
@StringRes resId: Int,
) {
val snackBar = makeSnackBar(resId)
setSnackBarActionMoveToSetting(snackBar)
snackBar.show()
}

private fun makeSnackBar(): Snackbar {
private fun makeSnackBar(
@StringRes resId: Int,
): Snackbar {
return Snackbar.make(
binding.root,
R.string.snack_bar_require_photo_album_permission,
resId,
Snackbar.LENGTH_LONG,
)
}

private fun setSnackBarAction(snackBar: Snackbar) {
private fun setSnackBarActionMoveToSetting(snackBar: Snackbar) {
snackBar.setAction(R.string.snack_bar_move_to_setting) {
val uri = Uri.fromParts(PACKAGE_SCHEME, requireContext().packageName, null)
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(uri)
startActivity(intent)
}
}

private fun initGalleryLauncher() {
galleryLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val imageUris = extractImageUris(result.data)
if (imageUris.isNotEmpty()) {
uriSelectedListener.onUrisSelected(*imageUris.toTypedArray())
dismiss()
} else {
showToast("사진을 불러올 수 없습니다.")
}
}
}
}

private fun extractImageUris(intent: Intent?): List<Uri> {
val imageUris = mutableListOf<Uri>()
intent?.let {
Expand All @@ -124,54 +251,38 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler {
return imageUris
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentPhotoAttachBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
binding.handler = this
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

override fun onCameraClicked() {
showToast(getString(R.string.all_default_not_supported))
}

override fun onGalleryClicked() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
checkPermissionsAndLaunch(READ_MEDIA_IMAGES)
} else {
checkPermissionsAndLaunch(READ_EXTERNAL_STORAGE)
}
private fun createImageUri(): Uri? {
val timeStamp = SimpleDateFormat(FILENAME_DATE_FORMAT, Locale.KOREA).format(Date())
val content = createImageContent(timeStamp)
return requireActivity().contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
content,
)
}

private fun checkPermissionsAndLaunch(permission: String) {
val isPermissionGranted =
ContextCompat.checkSelfPermission(
requireContext(),
permission,
) == PackageManager.PERMISSION_GRANTED
if (isPermissionGranted) {
launchGallery()
} else {
requestPermissionLauncher.launch(permission)
private fun createImageContent(fileName: String): ContentValues =
ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "img_$fileName.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
}
}

companion object {
const val TAG = "PhotoAttachModalBottomSheet"
const val PACKAGE_SCHEME = "package"
private const val FILENAME_DATE_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private val CAMERA_REQUIRED_PERMISSIONS =
mutableListOf(
Manifest.permission.CAMERA,
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
private val GALLERY_REQUIRED_PERMISSION =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
READ_MEDIA_IMAGES
} else {
READ_EXTERNAL_STORAGE
}
}
}
8 changes: 7 additions & 1 deletion android/Staccato_AN/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,17 @@
<string name="mypage_update_profile_image_description">프로필 이미지 수정</string>
<string name="mypage_navigate_to_instagram_btn_description">인스타그램으로 이동</string>

// activity_camera_preview
<string name="camera_preview_image_description">촬영된 이미지 미리 보기</string>

// fragment_photo_attach
<string name="photo_attach_title">사진을 등록해 주세요</string>
<string name="photo_attach_camera">카메라 열기</string>
<string name="photo_attach_album">앨범에서 가져오기</string>
<string name="snack_bar_require_photo_album_permission">사진 및 동영상 액세스 권한을 허용해 주세요.</string>
<string name="snack_bar_require_photo_album_permission">카메라와 사진 및 동영상 접근 권한이 필요합니다.\n설정에서 권한을 허용해 주세요.</string>
<string name="snack_bar_camera_error">카메라 실행 중 에러가 발생했습니다.\n잠시 후에 다시 시도해주세요.</string>
<string name="snack_bar_gallery_error">사진을 불러올 수 없습니다.\n잠시 후에 다시 시도해주세요.</string>
<string name="snack_bar_no_camera">실행할 수 있는 카메라 앱이 없습니다.</string>
<string name="snack_bar_move_to_setting">설정으로 이동하기</string>

// dialog_delete_impossibility
Expand Down
Loading

0 comments on commit e911a10

Please sign in to comment.