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

Nov 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()    }}
@Composablefun ImportPhotoScreen(viewModel: ImportPhotoViewModel = viewModel()) {    val albums by viewModel.getAlbums(context = LocalContext.current).collectAsState(initial = listOf())    AlbumContent(albums = albums)}

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.