Android 14에서 사진/동영상 등, 미디어 파일에 대한 일부 액세스 권한이 추가되었습니다.
기존에 앱은 사용자에게 사진/동영상 파일의 전체 접근 권한을 요청했었는데요, 이번에 사용자는 앱에 일부 파일 접근 권한만 부여할 수 있도록 변경되었습니다.
1. Android 13 이하에서 미디어 파일 접근 권한
Android 12 이하에서 미디어 파일을 읽으려면 READ_EXTERNAL_STORAGE
권한이 필요합니다.
android.permission.READ_EXTERNAL_STORAGE
Android 13에서는 READ_EXTERNAL_STORAGE
권한은 더 이상 사용되지 않고, 아래 3개 권한으로 분리되었습니다.
android.permission.READ_MEDIA_IMAGES
android.permission.READ_MEDIA_VIDEOS
android.permission.READ_MEDIA_AUDIOS
2. Android 14 이상에서 미디어 파일 접근 권한
기존에 아래 두개 권한을 사용자에게 요청하면 모든 사진/동영상 파일을 읽을 수 있었습니다.
android.permission.READ_MEDIA_IMAGES
android.permission.READ_MEDIA_VIDEOS
Android 14에서도 이 부분은 바뀌지 않았는데요. 대신, 사용자가 앱에 모든 권한을 부여하지 않고, 일부 파일의 접근 권한만 부여할 수 있는 기능이 추가되었습니다.
아래 권한이 Android 14에서 추가된 새로운 권한이며, 사용자가 일부 파일 접근 권한만 앱에 부여하였을 때, 이 권한이 앱에 부여됩니다.
android.permission.READ_MEDIA_VISUAL_USER_SELECTED
3. 신규 기능 구현 방법
다음과 같이 OS에 따라서 아래와 같이 앱의 Manifest에 권한을 추가합니다.
READ_EXTERNAL_STORAGE
는 Android 12L 이하에서만 사용되기 때문에maxSdkVersion="32"
라고 명시
<!-- Devices running Android 12L (API level 32) or lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- To handle the reselection within the app on Android 14 (API level 34) -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
아래와 같이 권한을 사용자에게 요청할 수 있습니다.
- Android 13 이하에서는 미디어 권한(
READ_MEDIA_IMAGES
,READ_MEDIA_VIDEO
)만 요청 - Android 14 이상에서 미디어 권한과 함께
READ_MEDIA_VISUAL_USER_SELECTED
권한도 요청
// Register ActivityResult handler
val requestPermissions = registerForActivityResult(RequestMultiplePermissions()) { results ->
// Handle permission requests results
// See the permission example in the Android platform samples: https://github.com/android/platform-samples
}
// Permission request logic
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO))
} else {
requestPermissions.launch(arrayOf(READ_EXTERNAL_STORAGE))
}
만약 비디오 파일만 접근하려면 아래와 같이 READ_MEDIA_VIDEO
권한만 요청할 수 있습니다. 사진 파일만 접근하는 경우에는 READ_MEDIA_IMAGES
권한을 요청하시면 됩니다
// Allow the user to select only videos
requestPermissions.launch(arrayOf(READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))
아래와 같이 앱은 사진/동영상 접근 권한을 갖고 있는지 체크할 수 있습니다.
READ_MEDIA_IMAGES
,READ_MEDIA_VIDEO
권한을 갖고 있으면, 모든 사진/동영상 파일을 접근할 수 있음READ_MEDIA_VISUAL_USER_SELECTED
권한만 갖고 있으면, 일부 사진/동영상 파일만 접근할 수 있음
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
(
ContextCompat.checkSelfPermission(context, READ_MEDIA_IMAGES) == PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, READ_MEDIA_VIDEO) == PERMISSION_GRANTED
)
) {
// Full access on Android 13 (API level 33) or higher
} else if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
ContextCompat.checkSelfPermission(context, READ_MEDIA_VISUAL_USER_SELECTED) == PERMISSION_GRANTED
) {
// Partial access on Android 14 (API level 34) or higher
} else if (ContextCompat.checkSelfPermission(context, READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
// Full access up to Android 12 (API level 32)
} else {
// Access denied
}
4. 미디어 파일 Query
아래 예제는 MediaStore를 통해 사진/동영상 파일을 가져오는 예제입니다.
갖고 있는 권한에 따라서 아래와 같은 차이점이 있습니다.
- 전체 접근 권한을 갖고 있으면, 디바이스의 모든 사진/동영상 파일 정보가 리턴
- 일부 접근 권한만 갖고 있으면, 사용자가 허락한 사진/동영상 파일 정보들만 리턴
data class Media(
val uri: Uri,
val name: String,
val size: Long,
val mimeType: String,
)
// Run the querying logic in a coroutine outside of the main thread to keep the app responsive.
// Keep in mind that this code snippet is querying only images of the shared storage.
suspend fun getImages(contentResolver: ContentResolver): List<Media> = withContext(Dispatchers.IO) {
val projection = arrayOf(
Images.Media._ID,
Images.Media.DISPLAY_NAME,
Images.Media.SIZE,
Images.Media.MIME_TYPE,
)
val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Query all the device storage volumes instead of the primary only
Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
Images.Media.EXTERNAL_CONTENT_URI
}
val images = mutableListOf<Media>()
contentResolver.query(
collectionUri,
projection,
null,
null,
"${Images.Media.DATE_ADDED} DESC"
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(Images.Media._ID)
val displayNameColumn = cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(Images.Media.SIZE)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)
while (cursor.moveToNext()) {
val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
val name = cursor.getString(displayNameColumn)
val size = cursor.getLong(sizeColumn)
val mimeType = cursor.getString(mimeTypeColumn)
val image = Media(uri, name, size, mimeType)
images.add(image)
}
}
return@withContext images
}
5. 디바이스 업그레이드 시 동작
Android 13 디바이스에서 Android 14로 업그레이드 될 때, 이미 설치된 앱은 아래와 같이 동작합니다.
- 앱이
READ_MEDIA_IMAGES
,READ_MEDIA_VIDEO
권한을 갖고 있으면, Android 14에서도 이 권한은 유지되고 모든 사진/동영상 파일에 접근할 수 있음
Android 12 디바이스에서 Android 14로 업그레이드 될 때, 이미 설치된 앱은 아래와 같이 동작합니다.
- 앱이
READ_EXTERNAL_STORAGE
또는WRITE_EXTERNAL_STORAGE
권한을 갖고 있으면, Android 14에서READ_MEDIA_IMAGES
,READ_MEDIA_VIDEO
권한이 앱에 자동으로 부여되며 모든 사진/동영상 파일에 접근할 수 있음
6. 일부 파일 접근 권한의 유효기간
사용자가 앱에 1개의 파일 접근 권한을 부여했습니다. 앱은 이 파일에 접근할 수 있는데, 언제까지 접근할 수 있을까요? READ_MEDIA_VISUAL_USER_SELECTED
권한만 갖고 있다면 영원히 파일에 접근할 수 있을까요?
Android developers 문서를 보면, 접근 권한은 만료될 수 있기 때문에(사용자가 권한 회수 등), 더 이상 접근할 수 없는 경우 다시 사용자에게 요청할 수 있도록 앱을 구현해야 한다고 합니다.
따라서, 미디어 파일에 대한 URI를 저장했다가 다시 읽으면 안되고, 파일을 읽기 전에 Media API로 query를 하여 현재 접근 가능한지 확인 후 파일을 읽도록 구현이 되어야 합니다.
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력