2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android: アプリ外の画像一覧を取得する (対象範囲別ストレージ対応)

Last updated at Posted at 2021-07-18

Android 10 以上の対象範囲別ストレージ (Scoped Storage) 環境で、メディア領域にある画像一覧を取得します。

アプリ独自の画像選択 UI を実装するときなどは、画像選択 Intent に頼らず、MediaStoreAPI から画像一覧へアクセスする必要があります。

Android 9 以下でも MediaStoreAPI 経由でアクセスすることに変わりはないため、すべての Android で動作するような実装としています。

前提

事前に READ_EXTERNAL_STORAGE パーミッションを得ておきます。

ActivityResultContract を使ってパーミッションを要求する方法は以下の記事を参照してください。

実装

data class Page(
    val page: Int,
    val items: List<String>,
    val hasNext: Boolean
)

suspend fun Context.loadMediaImages(
    page: Int,
    pageSize: Int
): Page = withContext(Dispatchers.IO) {
    val images = mutableListOf<String>()
    var hasNext = false
    val readExternalStorageGranted = (checkSelfPermission(READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)
    if (
        Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT ||
        readExternalStorageGranted
    ) {
        // READ_EXTERNAL_STORAGE 権限があれば、MediaStoreAPI 経由で画像一覧をすべて読み取れる。
        // Android 10 以上では READ_EXTERNAL_STORAGE 権限がなければ自アプリで保存した画像一覧のみ読み取れる
        // Android 9 以下では READ_EXTERNAL_STORAGE 権限がなければ画像一覧は一つも読み取れない
        val collection = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) {
            // データ読み込みだけの場合は MediaStore.VOLUME_EXTERNAL が適切
            MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
        } else {
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        } // collection = "content://media/external/images/media" のような Content URI
        val cursor = contentResolver.query(
            collection,
            arrayOf(MediaStore.Images.Media._ID),
            null,
            null,
            "${MediaStore.Images.Media.DATE_MODIFIED} DESC"
        )
        if (cursor != null) {
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val first = pageSize * page
            val last = pageSize * (page + 1) - 1
            var position = first
            while (position <= last && cursor.moveToPosition(position)) {
                val id = cursor.getLong(idColumn)
                // image = "content://media/external/images/media/27" のような Content URI
                images.add(ContentUris.withAppendedId(collection, id).toString())
                position++
            }
            hasNext = (position <= cursor.count - 1)
            cursor.close()
        }
    }
    Page(page, images.toList(), hasNext)
}

解説

Android 10 以上では VOLUME_EXTERNAL Content を参照します。Android 9 以下では EXTERNAL_CONTENT_URI Content を参照します。実際にはどちらも content://media/external/images/media となるようでしたが、Android 10 以上では VOLUME_EXTERNAL Content URI を使うことが推奨されています。

VOLUME_EXTERNAL の説明は以下のドキュメントにあります。VOLUME_EXTERNAL は読み取り専用のデータベース、VOLUME_EXTERNAL_PRIMARY は読み書き可能なデータベースとしてアクセスするときに使います。

The VOLUME_EXTERNAL volume provides a view of all shared storage volumes on the device. You can read the contents of this synthetic volume, but you cannot modify the contents.
The VOLUME_EXTERNAL_PRIMARY volume represents the primary shared storage volume on the device. You can read and modify the contents of this volume.

contentResolver.query() で、画像ファイルの更新日降順で、コンテンツ ID だけを読み取るクエリを発行します。そのあと、ページング処理をしながらコンテンツ ID を読み取り、ContentUris.withAppendedId() で画像ファイルの Content URI を生成します。

ここで取得できた Content URI content://media/external/images/media/{id} が画像ファイルのファイルパスとなります。

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?