봄날은 갔다. 이제 그 정신으로 공부하자

android R Scoped Storage 2편 (미디어 저장소에서 이미지를 읽어오자) 본문

android Tip

android R Scoped Storage 2편 (미디어 저장소에서 이미지를 읽어오자)

길재의 그 정신으로 공부하자 2021. 2. 11. 11:11

이번 글은 아래 글에 이은 2편 입니다.

https://als2019.tistory.com/52

 

지난번 글에서 Scoped Storage에 대해 너무 뜬구름 잡듯이 설명하고 지나가서 저 스스로도 그래서 어떻게 저장소에서 이미지를 읽어오라는건데? 라는 질문일 들더라구요.

 

그래서 저장소에서 이미지를 읽어오느 샘플 소스를 직접 만들어 보았습니다.  ^______^v

샘플 소스는 아래 git lepo에 있습니다.

https://github.com/lee-kil-jae/MyGallery

 

샘플 소스 위주로 간략히 설명하도록 하겠습니다.

 

설명 전에…

해당 소스는 MVVM 패턴으로 제작되었으며 DI로는 KOIN을 이미지 라이브러리로는 coil을 사용하였습니다.

그외에 tedpermission 등 다양한 라이브러리를 사용했습니다.

샘플 소스에 추가된 라이브러리는 app 폴더의 build.gradle 파일을 참조하시면 됩니다.

 

제일 우선 해야 할 것. (저장소 접근 권한 받기)

샘플 소스에서는 저장소에 있는 이미지만 읽어올 예정이므로 manifest.xml 파일에 아래 permission만 추가해줍니다.

Android R부터는 이제 아래 permission에 대해 사용자로부터 권한을 부여받아야 합니다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

 

권한은 TedPermission을 사용해 아래와 같이 요청해서 받아 줍니다.

TedPermission.with(this)
    .setRationaleMessage(R.string.need_read_storage_permission)
    .setDeniedMessage(R.string.denied_read_storage_permission)
    .setPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)
    .setPermissionListener(object : PermissionListener {
        override fun onPermissionGranted() {
            Toast.makeText(this@MainActivity, R.string.granted_read_storage_permission, Toast.LENGTH_SHORT).show()
        }

        override fun onPermissionDenied(deniedPermissions: ArrayList<String>?) {
            Toast.makeText(this@MainActivity, R.string.denied_read_storage_permission, Toast.LENGTH_SHORT).show()
        }
    }).check()

 

 

저장소 이미지 읽어오기 (폴더 별 이미지 읽어오기)

저장소의 이미지를 한꺼번에 읽어오면 시간이 오래걸리므로 일반적인 갤러리 앱들처럼 우선 폴더별 이미지를 읽어오는 방식을 사용하도록 하겠습니다. 폴더별 이미지를 읽어오는 부분은 GalleryFolderViewModel class의 loadFolderInfo()함수 입니다.

 

프로젝션 및 정렬 순서를 정한 후 폴더 정보를 요청하는 query 함수를 호출합니다.

val PROJECTION_FOLDER = arrayOf(
    MediaStore.Images.ImageColumns.BUCKET_ID,
    MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
    MediaStore.Images.ImageColumns.DATA
)
val orderBy = MediaStore.Images.Media.DATE_TAKEN
val cursor = contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    PROJECTION_FOLDER,
    null,
    null,
    "$orderBy DESC"
)

 

가져온 폴더 정보를 LiveData 객체에 추가해줍니다.

cursor?.let { folderCursor ->
    var ids = mutableListOf<Int>()
    while (folderCursor.moveToNext()) {
        val folder = DataGalleryFolder(
            folderCursor.getInt(INDEX_BUCKET_ID),
            folderCursor.getString(INDEX_BUCKET_NAME),
            folderCursor.getString(INDEX_BUCKET_URL)
        )
        if (!ids.contains(folder.id)) {
            ids.add(folder.id)
            items.value.add(folder)
        }
    }

    ids.clear()
    folderCursor.close()
}

 

폴더에 포함되어 있는 이미지 갯수 정보를 따로 query를 해 읽어와야 하므로 아래와 같이 query를 요청해 읽어옵니다.

for (folder in items.value) {
    val searchPrarams = MediaStore.MediaColumns.BUCKET_ID + " = " + folder.id
    val columns = arrayOf(MediaStore.Images.Media._ID)
    val cursor = contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        columns,
        searchPrarams,
        null,
        null
    )
    folder.count = cursor?.count?:0
    cursor?.close()
}

 

이미지 보여주기

이렇게 읽어온 것을 아래 이미지와 같이 보여주기 위해 RecyclerView와 BindingAdapter를 만들어줍니다.

 

// layout.xml
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rcvFolders"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
    app:spanCount="6"
    folderViewModel="@{vmFolder}"
    folderItems="@{vmFolder.items}"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/toolbar"
    app:layout_constraintBottom_toBottomOf="parent"/>

// bindingAdapter
@BindingAdapter(value = ["folderItems", "folderViewModel"], requireAll = true)
@JvmStatic
fun initFolderListView(view : RecyclerView, items : List<DataGalleryFolder>, vm : GalleryFolderViewModel){
    view.adapter?.run {
        if (this is AdapterGalleryFolder) {
            this.items = items
            this.vm = vm
        }
    }?: run{
        AdapterGalleryFolder(items, vm)
            .apply {
                view.adapter = this
            }
    }

    (view.layoutManager as GridLayoutManager).setSpanSizeLookup(object: GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return when(position){
                0 -> 6
                1,2 -> 3
                else -> 2
            }
        }
    })
}

 

정리

android R(11)부터 강제로 적용되는 Scoped Storage 정책에 대응하려면 아래 내용을 반드시 유념해야 합니다.

  • 저장소 접근을 위해서는 READ_EXTERNAL_STORAGE 권한도 받아야 합니다.
  • 저장소 접근 시 이상하게 접근하지 말고 위 소스와 같이 MediaStore 를 통해서만 접근해야 합니다.

간혹 MediaStore를 통해 접근해도 안되는 경우가 있는데 이런 경우 소스를 보면 대부분 query 호출 시 

ID를 통한 접근이 아닌 Name을 통한 접근 등과 같이 잘 못된 접근인 경우가 많습니다.

R 이전에는 이런것도 허용했는데 R부터는 ID 통해서만 접근해야 합니다.

 

샘플 소스에는 폴더별 이미지 상세 불러오기까지 구현되어 있습니다. ^^

Comments