Android Find Albums in Photo Gallery

September 24, 2019
with image count and cover image
class Album(val id: String, val name: String, var count: Long = 0, var lastImageUri: Uri? = null)

NOTE: READ_EXTERNAL_STORAGE Permission is required.

Solution 1: Loop all Photos

Loop through all images to find a list of unique albums (bucket).

fun findAlbums(contentResolver: ContentResolver): List<Album> {

    val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    val projections = arrayOf(
        MediaStore.Images.ImageColumns._ID,
        MediaStore.Images.ImageColumns.BUCKET_ID,
        MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
        MediaStore.Images.ImageColumns.DATA
    )

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

    val findAlbums = HashMap<String, Album>()

    contentResolver.query(contentUri, projections, null, null, orderBy)?.use { cursor ->
        if (cursor.moveToFirst()) {
            val bucketIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)
            val bucketNameIndex =
                cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)
            val imageUriIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)

            do {
                val bucketId = cursor.getString(bucketIdIndex)

                val album = findAlbums[bucketId] ?: let {
                    val bucketName = cursor.getString(bucketNameIndex)
                    val lastImageUri = Uri.parse(cursor.getString(imageUriIndex))
                    val album = Album(
                        id = bucketId,
                        name = bucketName,
                        lastImageUri = lastImageUri
                    )
                    findAlbums[bucketId] = album

                    album
                }

                album.count++

            } while (cursor.moveToNext())

        // cursor.close()
    }

    return findAlbums.values.toList()
}

Solution 2: Use Group By

NOTE: GROUP BY statement is not officially documented by ContentResolver.query

NOTE: Tested to work on Android Q (API 29) Emulator

fun findAlbumsWithGroupBy(contentResolver: ContentResolver): List<Album> {

    val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    // display name might not be unique
    val projections = arrayOf(
        // "DISTINCT ${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"
        MediaStore.Images.ImageColumns.BUCKET_ID,
        MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
        "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 albums = mutableListOf<Album>()
    contentResolver.query(contentUri, projections, groupBy, null, null)?.use { cursor ->
        if (cursor.moveToFirst()) {
            val bucketIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)
            val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)
            val countIndex = cursor.getColumnIndexOrThrow("image_count")

            do {
                val bucketId = cursor.getString(bucketIdIndex)
                val bucketName = cursor.getString(nameIndex)
                val imageCount = cursor.getLong(countIndex)

                val album = Album(id = bucketId, name = bucketName, count = imageCount)
                albums.add(album)

                // MAYBE can be optimized further?
                val projections = arrayOf(
                    MediaStore.Images.ImageColumns._ID,
                    MediaStore.Images.ImageColumns.DATA
                )
                val selection = "${MediaStore.Images.ImageColumns.BUCKET_ID} == ?"
                val selectionArgs = arrayOf(
                    bucketId
                )
                val sortOrder = "${MediaStore.Images.ImageColumns.DATE_TAKEN} DESC LIMIT 1"
                contentResolver.query(contentUri, projections, selection, selectionArgs, sortOrder)?.use { imageCursor ->
                    if (imageCursor.moveToFirst()) {
                        val imageUriIndex = imageCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)
                        val imageUri = imageCursor.getString(imageUriIndex)

                        album.lastImageUri = Uri.parse(imageUri)
                    }
                    // imageCursor.close()
                }

            } while (cursor.moveToNext())
        }
        // cursor.close()
    }

    return albums
}

Solution 3: Use Distinct

If you just need a list of image albums name, you could use DISTINCT BUCKET_DISPLAY_NAME.

NOTE: This method is not reliable as there are possibilities of 2 albums (bucket) with the same name.

val projections = arrayOf(
    "DISTINCT ${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"
)
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.