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" }