본문 바로가기
안드로이드/코틀린

Android PhotoPicker

by 나이아카 2024. 8. 19.

https://developer.android.com/training/data-storage/shared/photopicker

 

사진 선택 도구  |  Android Developers

DataStore offers a more modern way of storing local data. You should use DataStore instead of SharedPreferences. Read the DataStore guide for more information. 이 페이지는 Cloud Translation API를 통해 번역되었습니다. 사진 선택 도구 컬

developer.android.com

 

 오늘 설명하려는 라이브러리입니다. 회사에서 적용하고자 하니, 이슈가 많아 정리를 위해 들고 왔습니다. 분명 안드로이드에서 적용하라고 권유에 강제에 골치아픈 녀석인데 정작 말썽이 많은 친구네요...


 일단 바텀시트 형태로 올라오는 것이 특징입니다.

private lateinit var photoPicker: ActivityResultLauncher<PickVisualMediaRequest>

    private fun initLauncher() {
        photoPicker = if (viewModel.maxSelectCount > 1) {
            registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(viewModel.maxSelectCount)) { uris ->
                //...
            }
        } else {
            registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { url ->
                //...
            }
        }
    }

 저는 위와 같이 코드를 분류했는데요. 일단 maxCount를 1로 준 상태에서 PickMultipleVisualMedia를 실행하면 앱이 터집니다(💣💥). 그래서 개인적으로는 그냥 각 화면 별로 다수 선택이나 단일 선택에 맞게 ActivityResultLauncher를 실행하는게 좋지 않을까 생각되지만, 이번에는 어쩔 수 없이 하나의 액티비티에서 처리해야해서 분기를 저렇게 나눴습니다. lateinit은 참 좋은 것 같습니다(아무말).

 

 공식 문서를 보면 PhotoPicker를 정상적으로 사용할 수 없는 경우, ACTION_OPEN_DOCUMENT를 호출한다고 합니다. 실제로 코드를 보면 확인할 수 있는데요.

@CallSuper
override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
    // Check if Photo Picker is available on the device
    return if (isSystemPickerAvailable()) {
        Intent(MediaStore.ACTION_PICK_IMAGES).apply {
            type = getVisualMimeType(input.mediaType)
        }
    } else if (isSystemFallbackPickerAvailable(context)) {
        val fallbackPicker = checkNotNull(getSystemFallbackPicker(context)).activityInfo
        Intent(ACTION_SYSTEM_FALLBACK_PICK_IMAGES).apply {
            setClassName(fallbackPicker.applicationInfo.packageName, fallbackPicker.name)
            type = getVisualMimeType(input.mediaType)
        }
    } else if (isGmsPickerAvailable(context)) {
        val gmsPicker = checkNotNull(getGmsPicker(context)).activityInfo
        Intent(GMS_ACTION_PICK_IMAGES).apply {
            setClassName(gmsPicker.applicationInfo.packageName, gmsPicker.name)
            type = getVisualMimeType(input.mediaType)
        }
    } else {
        // For older devices running KitKat and higher and devices running Android 12
        // and 13 without the SDK extension that includes the Photo Picker, rely on the
        // ACTION_OPEN_DOCUMENT intent
        Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            type = getVisualMimeType(input.mediaType)

            if (type == null) {
                // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
                // intent with multiple mime types
                type = "*/*"
                putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
            }
        }
    }
}

 이런식으로 조건들이 나열되어 있고, 마지막에 모든 조건에서 사용 불가능한 경우 Intent.ACTION_OPEN_DOCUMENT를 플래그로 호출한다는 것을 볼 수 있습니다. 

 조건은 3가지이며 첫 번째로 isSystemPickerAvailable()의 경우 안드로이드 API 버전이 TIRAMISU 이상이거나, R이면서 ExtensionVersion이 2 이상인 경우 true를 설정합니다. 코드는 아래와 같습니다.

@SuppressLint("ClassVerificationFailure", "NewApi")
@JvmStatic
internal fun isSystemPickerAvailable(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        true
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        // getExtension is seen as part of Android Tiramisu only while the SdkExtensions
        // have been added on Android R
        getExtensionVersion(Build.VERSION_CODES.R) >= 2
    } else {
        false
    }
}

위와 같은 내용은 공식문서에 그대로 나와 있습니다. 신기하게도 안드로이드 버전이 11인 경우와 12인 경우를 제외하면 하위 단말에서는 SdkExtension을 수정하지 않아도 여러가지 방법을 통해 포토피커가 나오는데 반해, 두 경우에는 포토피커가 발생하지 않습니다. 거기다 공식문서에 나오는 백포팅 코드는 제 테스트 단말 기준으로는 쓸모가 없더라구요(모든 단말에서 쓸모가 없는지는 모르겠습니다...).

 그래서 SdkExtension을 사용하려면 아래와 같은 식으로 추가해줘야 합니다. 이때 SdkExtension은 1 ~ x까지 존재하는 게 아니라 특정 숫자만 있으므로 가능한 숫자가 무엇인지 SDK Manager를 통해 꼭 확인한 후, 작업하시길 바랍니다. 그렇지 않으면 Could not find compile target android-34-ext4 for modules :app 이런 느낌의 에러를 확인하실 수 있습니다.

compileSdkVersion 34
compileSdkExtension 10

 물론 여기까지 작업한다고 해서 PhotoPicker가 안드로이드 11이나 12에서 동작한다는 보장은 없습니다. 실제로 제 테스트 단말에서는 공식 문서에 존재하는 모든 코드(2024.08.16일 기준)를 다 넣었지만, 그래도 문서 편집기가 뜹니다. 원인과 해결방안을 찾는다면 공유하도록 하겠습니다.

 

그 다음은 isSystemFallbackPickerAvailable() 입니다. 

@JvmStatic
internal fun isSystemFallbackPickerAvailable(context: Context): Boolean {
    return getSystemFallbackPicker(context) != null
}

@Suppress("DEPRECATION")
@JvmStatic
internal fun getSystemFallbackPicker(context: Context): ResolveInfo? {
    return context.packageManager.resolveActivity(
        Intent(ACTION_SYSTEM_FALLBACK_PICK_IMAGES),
        PackageManager.MATCH_DEFAULT_ONLY or PackageManager.MATCH_SYSTEM_ONLY
    )
}

 

 코드는 위와 같이 되어 있네요. getSystemFallbackPicker가 null이 아니면 사용이 가능하다고 보는 것 같습니다. resolveActivity는 Android 11 미만에서 사용이 가능하며, 앱에 설치된 패키지 중 파라미터로 받은 인텐트를 처리할 수 있는 앱이 있는지 확인 후 반환해주는 메소드입니다. Android 11부터는 제거되었기도 하고 다른 애플리케이션의 패키지 목록을 가져올 수 없게 되면서 getSystemFallbackPcicker가 항상 null을 반환합니다. 그러므로 Android 11 미만일 때 활용이 가능한 조건입니다.

 

마지막은 isGmsPickerAvailable() 인데요.

@JvmStatic
internal fun isGmsPickerAvailable(context: Context): Boolean {
    return getGmsPicker(context) != null
}

@Suppress("DEPRECATION")
@JvmStatic
internal fun getGmsPicker(context: Context): ResolveInfo? {
    return context.packageManager.resolveActivity(
        Intent(GMS_ACTION_PICK_IMAGES),
        PackageManager.MATCH_DEFAULT_ONLY or PackageManager.MATCH_SYSTEM_ONLY
    )
}

 코드는 위와 같습니다. 이 코드는 위에서 보았던 getSystemFallbackPicker와 거의 동일합니다. 그저 resolveActivity의 Intent Flag 값만이 다른 형태입니다. GMS는 Google Mobile Service로 Android 운영체제를 사용하는 기기들 중 Google의 서비스와 애플리케이션이 포함된 패키지를 의미합니다. GMS는 일반적으로 Google 인증을 받은 기기에서 사용할 수 있으며, Google Play Store와 Google의 다른 앱들이 포함되어 있습니다. 흔히 아는 Android 운영체제는 AOSP로 Android Open Source Project의 범주에 속하는 라인으로, OS 그 자체만을 의미하는데, 갤럭시와 같은 기종을 구매했을 때 기본적으로 동봉되는 Chrome이나 Gmail등은 전부 GMS 라이센스가 갤럭시에 존재하기 때문에 내장되어 있는 것으로 생각하시면 되겠습니다.

 중요한 건 GMS가 아니라, SystemPicker가 사용될 수 없는 경우 GMS의 피커를 발생시키도록 작업되어 있다는 것입니다. 물론 이마저도 Android 31이상에서는 resolveActivity가 동작하지 않으니 사용할 수 없지만, 하위버전에서는 기기에 GMS가 존재한다면 GMS에서 제공해주는 이미지피커를 띄우게 됩니다. 

 

 내부 코드를 보고 나면 하위 버전에서는 Intent를 통해 외부 패키지를 실행시키는 것을 확인할 수 있습니다. 그래서 실제로 디버그 모드로 안드로이드 스튜디오에서 실행하다보면, 앱이 종료된 상태에서 포토피커만 존재하는 형태의 모습이 보일 때도 있습니다. 그런데 이는 Android 33에서도 발생한 적 있는 이슈라, 포토 피커 자체가 외부 앱을 호출하는 형태로 작업이 진행되는 것으로 추측됩니다(이 부분에 대해서는 추측이 아니라 정확한 테스트를 해보고 싶네요...).

 


 권한을 없애기 위해 특정 뷰를 제공하고 사용하라고 권장하는 것은 좋은데, 커스텀이 어려우면 무슨 의미가 있나 생각이 듭니다. 결국 UI 마저 제한하는 셈인데 안드로이드 개발자가 점점 더 할 수 있는 일이 없어지고 있는 것 같습니다.

 편한 건 좋은데, 그럼 더 숙련된 개발자가 필요한가...에 대한 의문은 남게 되네요. 당연히 안드로이드에 대해 이해를 잘 하고 많은 경험이 있는 사람이 새롭게 배우는 사람보다 잘하는 건 상식적이긴 하지만, 가성비에 대한 의문이...

 

댓글