Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 카메라 촬영 기능 연결 #514 #524

Merged
merged 9 commits into from
Oct 24, 2024
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
Loading