-
MediaStore File 다루기 (Android ScopedStorage)Android 2019. 12. 11. 12:33반응형
https://github.com/DNights/AndroidScopedStorageSample
DNights/AndroidScopedStorageSample
Android scoped storage sample project. Contribute to DNights/AndroidScopedStorageSample development by creating an account on GitHub.
github.com
SDK 29(Android Q/Android 10) 부터 ScopedStorage가 적용되어 외부 저장소에 접근이 제한되었습니다.
https://developer.android.com/training/data-storage/files/external-scoped 앱별 디렉터리의 경우 앱 제거시 저장된 파일 들이 같이 삭제가 됩니다.
미디어 컬랙션의 경우 지정된 위치의 폴더(DCIM, Pictures ...)에만 접근이 가능합니다.
다운로드 폴더의 경우 저장소 엑세스 프레임워크(SAF) 를 통해서 접근이 가능합니다.
그외 기존에 사용하던 모든 접근은 차단 됩니다.
현재 일시적으로 아래와 같이 menifest 옵션으로 기존과 동일하게 처리를 할 수 있습니다.
<manifest ... > <!-- This attribute is "false" by default on apps targeting Android 10 or higher. --> <application android:requestLegacyExternalStorage="true" ... > ... </application> </manifest>
하지만 차후 ScopedStorage로 변경 될 것이기 대문에 미리 확인하여 적용테스트를 해보도록 하겠습니다.
우선 미디어 컬랙션 부터 다루도록 하겠습니다.
[파일 탐색/읽기]
fun getFileList(context: Context, type: MediaStoreFileType): List<MediaFileData> { val fileList = mutableListOf<MediaFileData>() val projection = arrayOf( MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.DATE_TAKEN ) val sortOrder = "${MediaStore.Files.FileColumns.DATE_TAKEN} DESC" val cursor = context.contentResolver.query( type.externalContentUri, projection, null, null, sortOrder ) cursor?.use { val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) val dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_TAKEN) val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) val dateTaken = Date(cursor.getLong(dateTakenColumn)) val displayName = cursor.getString(displayNameColumn) val contentUri = Uri.withAppendedPath( type.externalContentUri, id.toString() ) Log.d( "test", "id: $id, display_name: $displayName, date_taken: $dateTaken, content_uri: $contentUri\n" ) fileList.add(MediaFileData(id, dateTaken, displayName, contentUri)) } } return fileList } enum class MediaStoreFileType( val externalContentUri: Uri, val mimeType: String, val pathByDCIM: String ) { IMAGE(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*", "/image"), AUDIO(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/*", "/audio"), VIDEO(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/*", "/video"); }
작성한 샘플 코드를 보면 위와 같이 작성되어 있으며
context 의 getContentResolver 를 얻어서 query를 통해 미디어 파일들을 탐색할 수 있습니다.
val cursor = context.contentResolver.query( type.externalContentUri, projection, null, null, sortOrder )
externalContentUri의 경우 파일 타입별로 아래와 같습니다.
이미지의 경우 MediaStore.Images.Media.EXTERNAL_CONTENT_URI
오디오의 경우 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
비디오의 경우 MediaStore.Video.Media.EXTERNAL_CONTENT_URI
파일 타입별로 조회가 가능합니다.
p.s (2019/12/11)
조회시 넘어오는 데이터 중에서 Date의 경우 어떤 것으로 조회를 하여도 1970-1-1 Thu Jan 01 09:00:00 GMT+09:00 1970 의 Date 초기값으로 지정되어 있어 작동상에 문제가 있는것으로 보입니다.
파일을 읽을경우 Uri를 통해서 접근하여 InputStream 등의 방법으로 파일을 읽을수 있습니다.
val contentUri = Uri.withAppendedPath( type.externalContentUri, id.toString() )
해당 파일의 Uri 는 위애서 설명한 externalContentUri + id 입니다.
이미지파일을 예로 들면 MediaStore.Images.Media.EXTERNAL_CONTENT_URI + id 이며
실제 로그상에서 String으로 출력시 content://media/external/images/media/11 와 같이 표시 됩니다.
[파일 생성]
fun createFile( context: Context, fileName: String, fileType: MediaStoreFileType, fileContents: ByteArray ) { val contentValues = ContentValues() /** * image allowed directories are [DCIM, Pictures] * audio allowed directories are [Alarms, Music, Notifications, Podcasts, Ringtones] * video allowed directories are [DCIM, Movies] */ when(fileType){ MediaStoreFileType.IMAGE ->{ contentValues.put( MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + fileType.pathByDCIM ) } MediaStoreFileType.AUDIO ->{ contentValues.put( MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + fileType.pathByDCIM ) } MediaStoreFileType.VIDEO ->{ contentValues.put( MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + fileType.pathByDCIM ) } } contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName) contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, fileType.mimeType) contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1) val uri = context.contentResolver.insert( fileType.externalContentUri, contentValues ) val parcelFileDescriptor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { context.contentResolver.openFileDescriptor(uri!!, "w", null) } else { TODO("VERSION.SDK_INT < KITKAT") } val fileOutputStream = FileOutputStream(parcelFileDescriptor!!.fileDescriptor) fileOutputStream.write(fileContents) fileOutputStream.close() contentValues.clear() contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0) context.contentResolver.update(uri, contentValues, null, null) }
파일 생성의 경우 ContentValues 를 통해서 파일을 생성할 수 있습니다.
생성시 MediaStore.Files.FileColumns.RELATIVE_PATH, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.MIME_TYPE 등의 항목을 추가할 수 있습니다.
MediaStore.Files.FileColumns.RELATIVE_PATH 의 경우 DCIM, Pictures만 접근 가능하며
그외의 경로를 접근시 아래와 같이 애러를 발생 시키며 접근이 불가능하게 됩니다.
java.lang.IllegalArgumentException: Primary directory (invalid) not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]
접근 가능한 폴더는 파일 타입별로 아래와 같습니다.
이미지 - DCIM, Pictures
오디오 - Alarms, Music, Notifications, Podcasts, Ringtones
비디오 - DCIM, Movies
[파일 삭제]
private var removeUri = Uri.EMPTY fun removeMediaFile(context: Context, uri: Uri) { try { uri.let { context.contentResolver.delete(uri, null, null) Log.d("test", "Removed MediaStore: $it") } (rv_file_list.adapter as MediaFileAdepter).setFileList(getFileList(this, curType)) } catch (e: RecoverableSecurityException) { val intentSender = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { e.userAction.actionIntent.intentSender } else { TODO("VERSION.SDK_INT < O") } intentSender?.let { ActivityCompat.startIntentSenderForResult( context as Activity, intentSender, DELETE_PERMISSION_REQUEST, null, 0, 0, 0, null ) } } removeUri = uri } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && requestCode == DELETE_PERMISSION_REQUEST) { removeMediaFile(this, removeUri) } }
파일을 삭제시 해당 앱에서 생성하고 권한이 있는 앱의 경우 삭제시 바로 삭제가 가능합니다.
다른 앱의 파일의 경우 삭제를 시도시 RecoverableSecurityException 발생합니다.
이경우 userAction.actionIntent.intentSender 를 통해서 startIntentSenderForResult 를 사용하여 사용자에게 권한이 없는 파일의 삭제여부를 전달할 수 있습니다.
요청시 아래와 같은 창이 표시되며 삭제 여부를 사용자에게 물어 봅니다.
허용을 누르게 되면 onActivityResult 통해서 삭제의 허용여부에 대한 답변과 권한을 가지고 옵니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && requestCode == DELETE_PERMISSION_REQUEST) { removeMediaFile(this, removeUri) } }
허용 이후 다시 removeMediaFile 을 호출하면 해당파일을 삭제합니다.
p.s (2019/12/11)
현재시점을 기준으로 삭제를 한뒤 파일을 조회하면 query 상으로는 삭제가 되었으나 실제 파일을 삭제되지 않고 아래와 같은 에러를 표시합니다. 정상적으로 삭제를 못하는 버그가 있는 것으로 확인 됩니다.
E/MediaProvider: Couldn't delete /storage/emulated/0/Pictures/Screenshots/Screenshot_20191211-093326.png
android.app.RecoverableSecurityException: dev.dnights.scopedstoragesample has no access to content://media/external/images/media/119https://issuetracker.google.com/issues/142270549
https://issuetracker.google.com/issues/145348304
p.s2(2019/12/11)
앱을 삭제후 다시 설치했을때 재설치 이전에 생성된 파일도 권한이 없는 파일로 인식하여 삭제여부를 물어봅니다.
Storage Access Framwork 다루기 (Android ScopedStorage)
https://github.com/DNights/AndroidScopedStorageSample DNights/AndroidScopedStorageSample Android scoped storage sample project. Contribute to DNights/AndroidScopedStorageSample development by creati..
dnight.tistory.com
[참고링크]
https://developer.android.google.cn/training/data-storage/files/external-scoped?hl=ko
범위가 지정된 외부 저장소 액세스 관리하기 | Android Developers
사용자에게 파일의 더 많은 권한을 제공하고 파일이 복잡해지는 것을 제한하기 위해, Android 10(API 레벨 29) 이상을 타겟팅하는 앱은 외부 저장소로 범위가 지정된 액세스 또는 범위 지정 저장소가 기본적으로 부여됩니다. 이러한 앱은 저장소의 앱별 디렉터리(Context.getExternalFilesDir()을 사용해 액세스함) 및 특정 유형의 미디어를 볼 수 있습니다. 앱에서 앱별 디렉터리나 MediaStore에 있지 않은 파일에 액세스해야 하는
developer.android.google.cn
안드로이드 Q Scoped Storage 이해하기
개발자를 위한 안드로이드 Q #2 | 개발자를 위한 안드로이드 Q #6 안드로이드 Q Scoped Storage에서 살아남기 시작하기 전에... 본 포스트는 Scoped Storage 관련된 내용을 다루며, 3월 14일 공개된 안드로이드 Q 베타 버전 기준으로 작성되었습니다. 이후, 정식 버전에서는 기능 및 API가 변경될 수 있으며, 기능에 관한 소감이나 의견은 개인적인 의견으로 회사의 공식
brunch.co.kr
https://brunch.co.kr/@huewu/14
안드로이드 Q Scoped Storage에서 살아남기
개발자를 위한 안드로이드 Q #6 | 시작하기 전에 이 포스트는 안드로이드 Q 베타 2 패치 버전을 기준으로 작성되었습니다. Q 정식 버전에서는 기능 및 API가 변경될 수 있습니다. 기능에 관한 소감이나 의견은 개인적인 의견으로 회사의 공식 의견과는 다를 수 있습니다. 본 포스트는 개발자를 위한 안드로이드 Q #2편 내용을 기반으로 Scoped Storage에서 파일을 읽고 쓰는 방법과 앱
brunch.co.kr
이제는 Android Scoped Storage를 준비해야 할 때
Android 10에서 외부저장소에 대해 Scoped Storage Mode가 소개되었습니다. 이에 영향을 받는 애플리케이션은 2020년 상반기까지는 Scoped mode에 대한 준비가 되어있어야 합니다.
medium.com
[Android] Q Scoped Storage - MediaStore로 화면 스크린샷 저장하기 - Android Q
2019.10.29 안드로이드 Q부터 Storage정책이 변경되었습니다. 우선 기존 Q 이전에서는 어떻게 적용되었는지, Q 이후에는 어떻게 적용되는지 살펴보겠습니다. ● Android Q 이전 개별 앱만 접근 가능 외부 저장소..
hjiee.tistory.com
https://codechacha.com/ko/android-mediastore-insert-media-files/
안드로이드 - MediaStore에 미디어 파일 저장하는 방법 | chacha
Media provider는 단말에 저장된 이미지, 동영상, 오디오 파일의 정보를 제공하는 프로바이더입니다. Scoped Storage가 적용되면서 미디어 데이터를 저장할 때는 MediaStore를 이용하기를 권장하고 있습니다. P와 Q에서 MediaStore에 데이터를 저장하는 방법을 알아보고, 차이점을 알아보겠습니다.
codechacha.com
https://codechacha.com/ko/android-mediastore-read-media-files/
안드로이드 - MediaStore에서 미디어 파일 정보 읽기 | chacha
Media provider는 단말에 저장된 이미지, 동영상, 오디오 파일의 정보를 제공하는 프로바이더입니다. MediaStore는 앱이 Media provider가 제공하는 파일들을 접근할 수 있도록 도와주는 API들의 묶음입니다. MediaStore의 API들이 어떤 의미인지 이해하고, 그 API를 통해 Media provider에 쿼리를 하여 데이터를 얻으면 됩니다.
codechacha.com
https://choidev-1.tistory.com/73
[안드로이드] MediaStore 파일 저장
안녕하세요 초이입니다. 안드로이드 Q의 Scoped Storage가 발표되면서 외부저장소의 공용 공간에 파일을 쓰거나 읽기를 원하면 MediaStore를 사용하라고 명시가 되버렸습니다. 그 기념으로 사용예제만 글로 작성하..
choidev-1.tistory.com
https://choidev-1.tistory.com/74
[안드로이드] MediaStore 파일 읽기
안녕하세요 초이입니다. 저번 게시물인 MediaStore를 활용하여 파일 쓰기 예제를 이어 이번 시간에는 파일 읽기 코드를 보도록 하겠습니다. MideaStore 파일 읽기 - 이미지 파일 읽기 private void readFile() { U..
choidev-1.tistory.com
반응형'Android' 카테고리의 다른 글
Android Learning Links (안드로이드 학습 링크) (0) 2020.01.07 Android 고유식별자 (unique identifiers) (0) 2020.01.02 Android Studio Editor 폰트(글꼴) 변경 (0) 2019.12.17 Storage Access Framwork 다루기 (Android ScopedStorage) (1) 2019.12.13 Android R8 Compiler (0) 2019.12.05 Android Studio Layout Inspector (0) 2019.12.05 Android Decompile Setting (0) 2019.11.28 Declarative UI Patterns (0) 2019.10.26