-
Storage Access Framwork 다루기 (Android ScopedStorage)Android 2019. 12. 13. 11:25반응형
https://github.com/DNights/AndroidScopedStorageSample
Storerage Accesss Framwork(SAF) 를 이용하여 파일을 다루는 방법에 대한 내용입니다.
SAF의 파일 구조는 Root 를 지정하여 그 하위의 Document들을 트리형태로 가진 구조를 입습니다.
제어 방식은 Intent를 통해서 Open Document, Create Document 의 Action 을 요청하여 System 에서 사용자에게 파일 열기, 파일 생성의 권한과 범위를 지정하는 UI를 띄우고 결과를 onActivityResult를 통해서 전달해주는 방식으로 되어 있습니다.
[파일 읽기/탐색]
private val OPEN_DIRECTORY_REQUEST_CODE = 1000 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_saf) val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION } } else { TODO("VERSION.SDK_INT < LOLLIPOP") } startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE) }
파일을 읽거나 탐색시 ACTION_OPEN_DOCUMENT_TREE 또는 ACTION_OPEN_DOCUMENT 를 통해서 Intent로 요청할 수 있습니다.
Intent로 요청시 위의 스크린샷처럼 엑세스 가능한 범위를 사용자에게 선택할 수 있는 System UI 가표시되며 사용자가 선택한 파일 또는 폴더를 Root로하여 uri를 onActivityResult 로 응답받습니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) { val directoryUri = data?.data ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { contentResolver.takePersistableUriPermission( directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) } getFileList(directoryUri) } }
응답받은 uri는 사용자가 선택한 파일 또는 폴더의 uri 이며 해당 uri 상위로는 접근이 불가능 합니다.
private fun getFileList(directoryUri: Uri) { val documentsTree = DocumentFile.fromTreeUri(application, directoryUri) ?: return val childDocuments = documentsTree.listFiles() tv_cur_path.text = directoryUri.path val fileList = childDocuments.map { SAFFileData( it.name ?: "unknown name", it.type ?: "unknown type", it.uri, it.isDirectory, it.parentFile?.uri ?: Uri.EMPTY ) } (rv_saf_file_list.adapter as SAFFileAdepter).setFileList(fileList) }
전달받은 uri를 가지고 DocumentFile의 트리를 얻을수 있으며 하위트리의 DocumentFile의 정보도 얻을 수 있습니다.
[파일 생성]
private val WRITE_REQUEST_CODE: Int = 1100 private fun createFile(fileName: String, mimeType: String) { val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = mimeType putExtra(Intent.EXTRA_TITLE, fileName) } } else { TODO("VERSION.SDK_INT < KITKAT") } startActivityForResult(intent, WRITE_REQUEST_CODE) }
파일 생성의 경우에도 Intent를 통해 요청 할 수 있습니다. ACTION_CREATE_DOCUMENT를 사용합니다.
ACTION_OPEN_DOCUMENT_TREE 요청과 동일하게 System UI 가 표시되고 저장하고자 하는 파일명과 저장 버튼이 뜹니다.
파일 생성 위치를 지정하는것은 파일읽기/탐색에서 지정한 범위와는 별개의 권한이기 때문에
읽기 범위 이외에 곳에서 생성을 하여도 정상적으로 권한을 획득하여 생성됩니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { data?.data?.also { uri -> Log.i("test", "Uri: $uri") writeFile(uri, "temp".toByteArray()) } } }
저장버튼을 누르뒤 onActivityResult를 통해서 사용자가 선택한 저장위치의 uri를 받을수 있습니다.
private fun writeFile(uri: Uri, data: ByteArray) { contentResolver.openFileDescriptor(uri, "w").use { FileOutputStream(it!!.fileDescriptor).use { fos -> fos.write(data) fos.close() } } }
응답받은 uri로 FileOutputStream을 통해서 파일을 쓸수 있습니다.
[파일 삭제]
private fun removeFile(uri: Uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { DocumentsContract.deleteDocument(contentResolver, uri) } Toast.makeText(applicationContext, "Remove File : $uri", Toast.LENGTH_SHORT).show() }
파일 삭제의 경우 삭제하고자하는 uri를 contentResolver 를 통해서 삭제 할수 있습니다.
다만 파일읽기/탐색에서 지정한 범위를 벗어나거나 앱에서 생성한 파일 이외에는 하위의 에러가 발생합니다.
java.lang.SecurityException: Permission Denial: writing com.android.externalstorage.ExternalStorageProvider uri content://com.android.externalstorage.documents/document/primary%3Atemp_file%20(1) from pid=6739, uid=10343 requires android.permission.MANAGE_DOCUMENTS, or grantUriPermission()
[파일 권한 저장]
파일을 생성하거나 열거나 할 경우 매번 Intent를 통해서 System UI를 표시할경우 사용자가 앱을 이용하는데 불편할수 있습니다.
그래서 최초에 사용자가 SystemUI를 통해서 지정한 뒤 onActivityResult 를 통해서 응답받은 Uri를 contentResolver.takePersistableUriPermission 통해서 권한을 얻은뒤 SharedPreferences 등의 저장소에 Uri를 저장하여 매번 Intent 요청없이 파일을 제어할 수 있습니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { contentResolver.takePersistableUriPermission(directoryUri,Intent.FLAG_GRANT_READ_URI_PERMISSION) }
[참고 링크]
https://developer.android.com/guide/topics/providers/document-provider
https://github.com/android/storage-samples
https://codechacha.com/ko/android-storage-access-framework/
반응형'Android' 카테고리의 다른 글
Android KeyStore 정보를 환경 변수로 저장/사용 (for.Mac) (0) 2020.01.23 Android Learning Links (안드로이드 학습 링크) (0) 2020.01.07 Android 고유식별자 (unique identifiers) (0) 2020.01.02 Android Studio Editor 폰트(글꼴) 변경 (0) 2019.12.17 MediaStore File 다루기 (Android ScopedStorage) (6) 2019.12.11 Android R8 Compiler (0) 2019.12.05 Android Studio Layout Inspector (0) 2019.12.05 Android Decompile Setting (0) 2019.11.28