diff --git a/android/Staccato_AN/app/build.gradle.kts b/android/Staccato_AN/app/build.gradle.kts index b3ead0262..50199b158 100644 --- a/android/Staccato_AN/app/build.gradle.kts +++ b/android/Staccato_AN/app/build.gradle.kts @@ -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 { diff --git a/android/Staccato_AN/app/src/main/AndroidManifest.xml b/android/Staccato_AN/app/src/main/AndroidManifest.xml index c10b1bacf..d5c54f8c8 100644 --- a/android/Staccato_AN/app/src/main/AndroidManifest.xml +++ b/android/Staccato_AN/app/src/main/AndroidManifest.xml @@ -11,9 +11,9 @@ - + @@ -24,8 +24,9 @@ - + + + private lateinit var requestGalleryPermissionLauncher: ActivityResultLauncher> + private lateinit var requestCameraPermissionLauncher: ActivityResultLauncher> private lateinit var galleryLauncher: ActivityResultLauncher + private lateinit var cameraLauncher: ActivityResultLauncher 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 } @@ -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> = + 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) @@ -73,21 +170,66 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler { galleryLauncher.launch(intent) } + private fun checkPermissionsAndLaunch( + permissions: Array, + requestPermissionLauncherOnNotGranted: ActivityResultLauncher>, + 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) @@ -95,21 +237,6 @@ class PhotoAttachFragment : BottomSheetDialogFragment(), PhotoAttachHandler { } } - 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 { val imageUris = mutableListOf() intent?.let { @@ -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 + } } } diff --git a/android/Staccato_AN/app/src/main/res/values/strings.xml b/android/Staccato_AN/app/src/main/res/values/strings.xml index 304d5cdeb..516839ac4 100644 --- a/android/Staccato_AN/app/src/main/res/values/strings.xml +++ b/android/Staccato_AN/app/src/main/res/values/strings.xml @@ -160,11 +160,17 @@ 프로필 이미지 수정 인스타그램으로 이동 + // activity_camera_preview + 촬영된 이미지 미리 보기 + // fragment_photo_attach 사진을 등록해 주세요 카메라 열기 앨범에서 가져오기 - 사진 및 동영상 액세스 권한을 허용해 주세요. + 카메라와 사진 및 동영상 접근 권한이 필요합니다.\n설정에서 권한을 허용해 주세요. + 카메라 실행 중 에러가 발생했습니다.\n잠시 후에 다시 시도해주세요. + 사진을 불러올 수 없습니다.\n잠시 후에 다시 시도해주세요. + 실행할 수 있는 카메라 앱이 없습니다. 설정으로 이동하기 // dialog_delete_impossibility diff --git a/android/Staccato_AN/gradle/libs.versions.toml b/android/Staccato_AN/gradle/libs.versions.toml index 5889126b1..089498d28 100644 --- a/android/Staccato_AN/gradle/libs.versions.toml +++ b/android/Staccato_AN/gradle/libs.versions.toml @@ -36,6 +36,7 @@ crashlytics = "3.0.2" mapsplatformSecretsGradlePlugin = "2.0.1" playServicesMaps = "19.0.0" viewpager2 = "1.1.0" +camerax = "1.1.0-beta01" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -76,6 +77,11 @@ coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" } play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" } +androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } +androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } +androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } +androidx-camera-extension = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }