Android List All Albums Gallery - Android 11 (API level 30)

November 7, 2021

Old tricks like using Group By and Distinct in contentResolver.query doesn’t work anymore.

Solution: Loop all Photos using contentResolver.query

class Gallery(val context: Context) {
    private val contentResolver by lazy {
        context.contentResolver
    }

    class Album(
        val id: String,
        val name: String,
        var count: Long = 0,
        var uri: Uri? = null,
        var file: File? = null
    )

    fun findAlbums(): List<Album> {

        // val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        val collection =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                MediaStore.Images.Media.getContentUri(
                    MediaStore.VOLUME_EXTERNAL
                )
            } else {
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            }

        // display name might not be unique
        // Android 11: IllegalArgumentException: Invalid column COUNT(_id) AS image_count
        // https://issuetracker.google.com/issues/130965914

        val projections = arrayOf(
            // "DISTINCT ${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"
            MediaStore.Images.ImageColumns._ID,
            MediaStore.Images.ImageColumns.DATE_TAKEN,
            MediaStore.Images.ImageColumns.BUCKET_ID,
            MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
            // MediaStore.Images.ImageColumns.DATA
            // "COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS image_count"
        )

        // val groupBy = "1) GROUP BY ${MediaStore.Images.ImageColumns.BUCKET_ID}, (${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"

        val orderBy = "${MediaStore.Images.ImageColumns.DATE_TAKEN} DESC"

        val findAlbums = HashMap<String, Album>()
        contentResolver.query(collection, projections, null, null, orderBy)?.use { cursor ->
            if (cursor.moveToFirst()) {
                val imageIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)
                val bucketIdIndex =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)
                val bucketNameIndex =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)

                do {
                    val bucketId = cursor.getString(bucketIdIndex)

                    val album = findAlbums[bucketId] ?: let {
                        val bucketName = cursor.getString(bucketNameIndex)
                        // val lastImageUri = Uri.parse(cursor.getString(imageUriIndex))
                        val imageId = cursor.getLong(imageIdIndex)
                        val uri = ContentUris.withAppendedId(
                            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                            imageId
                        )
                        val album = Album(
                            id = bucketId,
                            name = bucketName,
                            uri = uri,
                            count = 1
                        )
                        findAlbums[bucketId] = album

                        album
                    }

                    album.count++

                } while (cursor.moveToNext())
            }
        }

        return findAlbums.values.toList().sortedByDescending { it.count }
    }
}

Usage

val gallery = Gallery(context)
val albums: List<Gallery.Album> = gallery.findAlbums()

Flow

Implementation using Kotlin Flow

fun findAlbums(): Flow<List<Album>> = flow {
    // val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val collection =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            MediaStore.Images.Media.getContentUri(
                MediaStore.VOLUME_EXTERNAL
            )
        } else {
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        }

    // display name might not be unique
    // Android 11: IllegalArgumentException: Invalid column COUNT(_id) AS image_count
    // https://issuetracker.google.com/issues/130965914

    val projections = arrayOf(
        // "DISTINCT ${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"
        MediaStore.Images.ImageColumns._ID,
        MediaStore.Images.ImageColumns.DATE_TAKEN,
        MediaStore.Images.ImageColumns.BUCKET_ID,
        MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
        // MediaStore.Images.ImageColumns.DATA
        // "COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS image_count"
    )

    // val groupBy = "1) GROUP BY ${MediaStore.Images.ImageColumns.BUCKET_ID}, (${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"

    val orderBy = "${MediaStore.Images.ImageColumns.DATE_TAKEN} DESC"

    val findAlbums = HashMap<String, Album>()
    contentResolver.query(collection, projections, null, null, orderBy)?.use { cursor ->
        if (cursor.moveToFirst()) {
            val imageIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)
            val bucketIdIndex =
                cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)
            val bucketNameIndex =
                cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)

            do {
                val bucketId = cursor.getString(bucketIdIndex)

                val album = findAlbums[bucketId] ?: let {
                    val bucketName = cursor.getString(bucketNameIndex)
                    // val lastImageUri = Uri.parse(cursor.getString(imageUriIndex))
                    val imageId = cursor.getLong(imageIdIndex)
                    val uri = ContentUris.withAppendedId(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        imageId
                    )
                    val album = Album(
                        id = bucketId,
                        name = bucketName,
                        uri = uri,
                        count = 0
                    )
                    findAlbums[bucketId] = album
                    emit(findAlbums.values.toList().sortedByDescending { it.count })

                    album
                }

                album.count++

            } while (cursor.moveToNext())
        }
    }

    emit(findAlbums.values.toList().sortedByDescending { it.count })

    // return findAlbums.values.toList().sortedByDescending { it.count }
}

Usage in Composable

class ImportPhotoViewModel: ViewModel() {
    fun getAlbums(context: Context): Flow<List<Gallery.Album>> {
        val gallery = gallery ?: Gallery(context = context)
        return gallery.findAlbums()
    }
}
@Composable
fun ImportPhotoScreen(viewModel: ImportPhotoViewModel = viewModel()) {
    val albums by viewModel.getAlbums(context = LocalContext.current).collectAsState(initial = listOf())
    AlbumContent(albums = albums)
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.