Search
🥊

이미지 권한 설정 및 업로드

Date
2024/08/07
Part
안드로이드
Writer
Whether to upload blog
이미지 권한 설정 및 이미지 업로드 기능 구현 및 서버 통신에 대한 정리 글을 작성해보았습니다!

권한 설정

target sdk 33 부터 세분화되어 달라졌다.
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Kotlin
복사
세분화된 이미지 권한 참고: 공식문서  

실제 권한 설정 로직

PermissionManager
class PermissionManager( private val fragment: Fragment, private val onPermissionGranted: () -> Unit, private val onPermissionDenied: () -> Unit, ) { private val storagePermissions = arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, ) private val requestPermissionLauncher = fragment.registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), ) { permissions -> if (permissions.values.all { it }) { onPermissionGranted() } else { onPermissionDenied() } } fun requestPermissions() { if (isAndroid13OrAbove() || hasPermissions(fragment.requireContext(), storagePermissions)) { onPermissionGranted() } else { requestPermissionLauncher.launch(storagePermissions) } } fun isAndroid13OrAbove(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU private fun hasPermissions( context: Context, permissions: Array<String>, ): Boolean { return permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } } }
Kotlin
복사
Activity
permissionManager을 통해 권한 설정에 대한 로직만 구현
private lateinit var permissionManager: PermissionManager private val viewModel: OfferingWriteViewModel by viewModels { OfferingWriteViewModel.getFactory( offeringRepository = (requireActivity().application as ChongdaeApp).offeringRepository, ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setUpPermissionManager() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { initBinding(inflater, container) return fragmentBinding.root } override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) observeImageUploadEvent() } private fun setUpPermissionManager() { permissionManager = PermissionManager( fragment = this, onPermissionGranted = { onPermissionsGranted() }, onPermissionDenied = { onPermissionsDenied() }, ) } private fun observeImageUploadEvent() { viewModel.imageUploadEvent.observe(viewLifecycleOwner) { permissionManager.requestPermissions() } } private fun onPermissionsGranted() { showToast(R.string.permission_granted) pickImage() } private fun pickImage() { // TODO: 이미지 선택 기능 구현 (글 아래 로직 참고) } private fun onPermissionsDenied() { showToast(R.string.permission_denied) }
Kotlin
복사
Viewmodel
xml 에서 클릭이벤트가 일어나면 imageUploadEvent를 업데이트시켜서 activity에 전달
private val _imageUploadEvent = MutableLiveData<Unit>() val imageUploadEvent: LiveData<Unit> get() = _imageUploadEvent fun onUploadPhotoClick() { _imageUploadEvent.value = Unit }
Kotlin
복사
xml에서 databinding을 사용하여 click을 전달할 수 있다.
android:onClick="@{() -> vm.onUploadPhotoClick()}"
Kotlin
복사

실제 에뮬레이터를 통한 버전별 권한 동작 확인

sdk 33
sdk 29
sdk26

photo picker

그런데 티라미슈부터(33)는 이미지 피커를 사용한다면 권한이 필요가 없다.

private lateinit var permissionManager: PermissionManager private lateinit var pickMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setUpPermissionManager() initializePhotoPicker() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { initBinding(inflater, container) return fragmentBinding.root } override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { observeImageUploadEvent() } private fun initializePhotoPicker() { pickMediaLauncher = registerForActivityResult(PickVisualMedia()) { uri: Uri? -> handleMediaResult(uri) } } private fun launchPhotoPicker() { pickMediaLauncher.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) } private fun handleMediaResult(uri: Uri?) { if (uri != null) { Log.d("Picker", "URI: $uri") } else { Log.d("Picker", "아무것도 선택 안됨") } } private fun setUpPermissionManager() { permissionManager = PermissionManager( fragment = this, onPermissionGranted = { onPermissionsGranted() }, onPermissionDenied = { onPermissionsDenied() }, ) } private fun observeImageUploadEvent() { viewModel.imageUploadEvent.observe(viewLifecycleOwner) { // sdk33이상만 권한 허용 받기 if (permissionManager.isAndroid13OrAbove()) { launchPhotoPicker() } else { permissionManager.requestPermissions() } } } private fun onPermissionsGranted() { showToast(R.string.permission_granted) launchPhotoPicker() } private fun onPermissionsDenied() { showToast(R.string.permission_denied) }
Kotlin
복사
이런식으로 uri가 제대로 들어오는 것을 확인할 수 있다.
FileUtils
uri → file → Multipart 로 변경하는 객체를 구현
object FileUtils { fun getMultipartBodyPart( context: Context, uri: Uri, paramName: String, ): MultipartBody.Part? { val file = getFileFromUri(context, uri) ?: return null return getMultipartBodyPart(file, paramName) } private fun getFileFromUri( context: Context, uri: Uri, ): File? { val contentResolver: ContentResolver = context.contentResolver val fileName = getFileName(contentResolver, uri) val file = File(context.cacheDir, fileName) try { val inputStream: InputStream? = contentResolver.openInputStream(uri) val outputStream = FileOutputStream(file) inputStream?.copyTo(outputStream) inputStream?.close() outputStream.close() } catch (e: Exception) { e.printStackTrace() return null } return file } private fun getFileName( contentResolver: ContentResolver, uri: Uri, ): String { var name = "" val returnCursor = contentResolver.query(uri, null, null, null, null) returnCursor?.use { if (it.moveToFirst()) { val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) name = it.getString(nameIndex) } } return name } private fun getMultipartBodyPart( file: File, paramName: String, ): MultipartBody.Part { val requestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull()) return MultipartBody.Part.createFormData(paramName, file.name, requestBody) } }
Kotlin
복사

Fragment에서 사용한 예시

private fun initializePhotoPicker() { pickMediaLauncher = registerForActivityResult(PickVisualMedia()) { uri: Uri? -> handleMediaResult(uri) } } private fun observeImageUploadEvent() { viewModel.imageUploadEvent.observe(viewLifecycleOwner) { if (permissionManager.isAndroid13OrAbove()) { launchPhotoPicker() } else { permissionManager.requestPermissions() } } } private fun handleMediaResult(uri: Uri?) { if (uri != null) { val multipartBodyPart = FileUtils.getMultipartBodyPart(requireContext(), uri, "image") if (multipartBodyPart != null) { viewModel.uploadImageFile(multipartBodyPart) } else { showToast(R.string.error_file_conversion) } } }
Kotlin
복사

viewmodel 에서 uploadImageFile

서버와 통신을 통해 image의 multipart를 전달한다.
api service
// @Multipart를 꼭! 추가해야한다. @Multipart @POST("/주소주소") suspend fun postProductImageS3( @Part image: MultipartBody.Part, ): Response<ProductUrlResponse>
Kotlin
복사
repository (또는 datasource) 에서 해당하는 로직을 구현한다.
suspend fun saveProductImageS3(image: MultipartBody.Part): Result<ProductUrlResponse>
Kotlin
복사

실제 구현 PR

실제 프로젝트에서 이미지 권한과 업로드에 대한 기능을 구현한 pull request
216
pull

참고