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}
が画像ファイルのファイルパスとなります。