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)}