Glimpse: Introduce AlbumFlow
* AlbumViewerFragment can now get album's info on its own, so change
album argument to just bucket ID
* Since an album might be empty (e.g. our own bucket IDs) make the
thumbnail optional
Change-Id: Iab2fcb8c120ead658216f842309491edff5afa77
diff --git a/app/src/main/java/org/lineageos/glimpse/flow/AlbumFlow.kt b/app/src/main/java/org/lineageos/glimpse/flow/AlbumFlow.kt
new file mode 100644
index 0000000..3f2212c
--- /dev/null
+++ b/app/src/main/java/org/lineageos/glimpse/flow/AlbumFlow.kt
@@ -0,0 +1,160 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.glimpse.flow
+
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.core.os.bundleOf
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.lineageos.glimpse.R
+import org.lineageos.glimpse.ext.queryFlow
+import org.lineageos.glimpse.models.Album
+import org.lineageos.glimpse.models.MediaStoreMedia
+import org.lineageos.glimpse.query.*
+import org.lineageos.glimpse.utils.MediaStoreBuckets
+
+class AlbumFlow(
+ private val context: Context,
+ private val bucketId: Int,
+) : QueryFlow<Album>() {
+ override fun flowCursor(): Flow<Cursor?> {
+ val uri = MediaQuery.MediaStoreFileUri
+ val projection = MediaQuery.AlbumsProjection
+ val imageOrVideo =
+ (MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) or
+ (MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
+ val albumFilter = when (bucketId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> MediaStore.Files.FileColumns.IS_FAVORITE eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
+ MediaStore.Files.FileColumns.IS_TRASHED eq 1
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_REELS.id -> null
+
+ else -> MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
+ }
+ val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
+ val selectionArgs = bucketId.takeIf {
+ MediaStoreBuckets.values().none { bucket -> it == bucket.id }
+ }?.let { arrayOf(it.toString()) }
+ val sortOrder = "${MediaStore.Files.FileColumns.DATE_ADDED} DESC"
+ val queryArgs = Bundle().apply {
+ putAll(
+ bundleOf(
+ ContentResolver.QUERY_ARG_SQL_SELECTION to selection.build(),
+ ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
+ ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
+ ContentResolver.QUERY_ARG_SQL_LIMIT to 1,
+ )
+ )
+
+ // Exclude trashed media unless we want data for the trashed album
+ putInt(
+ MediaStore.QUERY_ARG_MATCH_TRASHED, when (bucketId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
+
+ else -> MediaStore.MATCH_EXCLUDE
+ }
+ )
+ }
+
+ return context.contentResolver.queryFlow(
+ uri,
+ projection,
+ queryArgs,
+ )
+ }
+
+ override fun flowData() = flowCursor().map {
+ mutableListOf<Album>().apply {
+ it?.use {
+ val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
+ val bucketIdIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
+ val displayNameIndex = it.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
+ val isFavoriteIndex =
+ it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
+ val isTrashedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
+ val mediaTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
+ val mimeTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)
+ val dateAddedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED)
+ val dateModifiedIndex =
+ it.getColumnIndex(MediaStore.Files.FileColumns.DATE_MODIFIED)
+ val widthIndex = it.getColumnIndex(MediaStore.Files.FileColumns.WIDTH)
+ val heightIndex = it.getColumnIndex(MediaStore.Files.FileColumns.HEIGHT)
+ val orientationIndex =
+ it.getColumnIndex(MediaStore.Files.FileColumns.ORIENTATION)
+ val bucketDisplayNameIndex =
+ it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
+
+ val cursorNotEmpty = it.moveToFirst()
+
+ val media = if (cursorNotEmpty) {
+ val id = it.getLong(idIndex)
+ val mediaBucketId = it.getInt(bucketIdIndex)
+ val displayName = it.getString(displayNameIndex)
+ val isFavorite = it.getInt(isFavoriteIndex)
+ val isTrashed = it.getInt(isTrashedIndex)
+ val mediaType = it.getInt(mediaTypeIndex)
+ val mimeType = it.getString(mimeTypeIndex)
+ val dateAdded = it.getLong(dateAddedIndex)
+ val dateModified = it.getLong(dateModifiedIndex)
+ val width = it.getInt(widthIndex)
+ val height = it.getInt(heightIndex)
+ val orientation = it.getInt(orientationIndex)
+
+ MediaStoreMedia.fromMediaStore(
+ id,
+ mediaBucketId,
+ displayName,
+ isFavorite,
+ isTrashed,
+ mediaType,
+ mimeType,
+ dateAdded,
+ dateModified,
+ width,
+ height,
+ orientation,
+ )
+ } else {
+ null
+ }
+
+ val bucketDisplayName = when (cursorNotEmpty) {
+ true -> it.getString(bucketDisplayNameIndex)
+ false -> null
+ }
+
+ val album = Album(
+ bucketId,
+ when (bucketId) {
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> context.getString(
+ R.string.album_favorites
+ )
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> context.getString(
+ R.string.album_trash
+ )
+
+ MediaStoreBuckets.MEDIA_STORE_BUCKET_REELS.id -> context.getString(
+ R.string.album_reels
+ )
+
+ else -> bucketDisplayName ?: Build.MODEL
+ },
+ media
+ )
+
+ add(album)
+ }
+ }.toList()
+ }
+}
diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
index 69013a6..331be7f 100644
--- a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
+++ b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
@@ -65,8 +65,8 @@
class AlbumViewerFragment : Fragment(R.layout.fragment_album_viewer) {
// View models
private val model: AlbumViewerViewModel by viewModels {
- album?.let {
- AlbumViewerViewModel.factory(requireActivity().application, it.id)
+ bucketId?.let {
+ AlbumViewerViewModel.factory(requireActivity().application, it)
} ?: AlbumViewerViewModel.factory(requireActivity().application)
}
@@ -95,6 +95,16 @@
}
}
}
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ model.album.collectLatest {
+ activity?.runOnUiThread {
+ toolbar.title = it.name
+ }
+ }
+ }
+ }
}
// MediaStore
@@ -138,7 +148,7 @@
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
requireActivity().menuInflater.inflate(
- when (album?.id) {
+ when (bucketId) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> R.menu.album_action_bar_trash
else -> R.menu.album_action_bar
},
@@ -266,7 +276,7 @@
}
// Arguments
- private val album by lazy { arguments?.getParcelable(KEY_ALBUM, Album::class) }
+ private val bucketId by lazy { arguments?.getInt(KEY_BUCKET_ID) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -276,14 +286,10 @@
appBarLayout.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(requireContext())
- album?.let {
- toolbar.title = it.name
- }
-
val appBarConfiguration = AppBarConfiguration(navController.graph)
toolbar.setupWithNavController(navController, appBarConfiguration)
- when (album?.id) {
+ when (bucketId) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
R.menu.fragment_album_viewer_toolbar_trash
else -> null
@@ -404,16 +410,16 @@
}
companion object {
- private const val KEY_ALBUM = "album"
+ private const val KEY_BUCKET_ID = "bucket_id"
/**
* Create a [Bundle] to use as the arguments for this fragment.
- * @param album The [Album] to display, if null, reels will be shown
+ * @param bucketId The [Album] to display's bucket ID, if null, reels will be shown
*/
fun createBundle(
- album: Album? = null,
+ bucketId: Int? = null,
) = bundleOf(
- KEY_ALBUM to album,
+ KEY_BUCKET_ID to bucketId,
)
/**
@@ -424,9 +430,9 @@
* @return A new instance of fragment [AlbumViewerFragment].
*/
fun newInstance(
- album: Album,
+ bucketId: Int? = null,
) = AlbumViewerFragment().apply {
- arguments = createBundle(album)
+ arguments = createBundle(bucketId)
}
}
}
diff --git a/app/src/main/java/org/lineageos/glimpse/models/Album.kt b/app/src/main/java/org/lineageos/glimpse/models/Album.kt
index f7d7cc7..691d3e9 100644
--- a/app/src/main/java/org/lineageos/glimpse/models/Album.kt
+++ b/app/src/main/java/org/lineageos/glimpse/models/Album.kt
@@ -17,7 +17,7 @@
data class Album(
val id: Int,
val name: String,
- val thumbnail: MediaStoreMedia,
+ val thumbnail: MediaStoreMedia? = null,
var size: Int = 0,
) : Comparable<Album>, Parcelable {
constructor(parcel: Parcel) : this(
diff --git a/app/src/main/java/org/lineageos/glimpse/recyclerview/AlbumThumbnailAdapter.kt b/app/src/main/java/org/lineageos/glimpse/recyclerview/AlbumThumbnailAdapter.kt
index 9e7161b..e56909f 100644
--- a/app/src/main/java/org/lineageos/glimpse/recyclerview/AlbumThumbnailAdapter.kt
+++ b/app/src/main/java/org/lineageos/glimpse/recyclerview/AlbumThumbnailAdapter.kt
@@ -59,8 +59,10 @@
R.plurals.album_thumbnail_items, album.size, album.size
)
- thumbnailImageView.load(album.thumbnail.uri) {
- memoryCacheKey("thumbnail_${album.thumbnail.id}")
+ thumbnailImageView.load(album.thumbnail?.uri) {
+ album.thumbnail?.let {
+ memoryCacheKey("thumbnail_${it.id}")
+ }
size(DisplayAwareGridLayoutManager.MAX_THUMBNAIL_SIZE)
placeholder(R.drawable.thumbnail_placeholder)
}
@@ -68,7 +70,7 @@
itemView.setOnClickListener {
navController.navigate(
R.id.action_mainFragment_to_albumViewerFragment,
- AlbumViewerFragment.createBundle(album)
+ AlbumViewerFragment.createBundle(album.id)
)
}
}
diff --git a/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt b/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt
index 8a5cf1f..24fddd7 100644
--- a/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt
+++ b/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt
@@ -6,6 +6,7 @@
package org.lineageos.glimpse.repository
import android.content.Context
+import org.lineageos.glimpse.flow.AlbumFlow
import org.lineageos.glimpse.flow.AlbumsFlow
import org.lineageos.glimpse.flow.MediaFlow
@@ -13,6 +14,8 @@
object MediaRepository {
fun media(context: Context, bucketId: Int) = MediaFlow(context, bucketId).flowData()
fun mediaCursor(context: Context, bucketId: Int) = MediaFlow(context, bucketId).flowCursor()
+ fun album(context: Context, bucketId: Int) = AlbumFlow(context, bucketId).flowData()
+ fun albumCursor(context: Context, bucketId: Int) = AlbumFlow(context, bucketId).flowCursor()
fun albums(context: Context) = AlbumsFlow(context).flowData()
fun albumsCursor(context: Context) = AlbumsFlow(context).flowCursor()
}
diff --git a/app/src/main/java/org/lineageos/glimpse/viewmodels/AlbumViewerViewModel.kt b/app/src/main/java/org/lineageos/glimpse/viewmodels/AlbumViewerViewModel.kt
index e3d2913..7b18dea 100644
--- a/app/src/main/java/org/lineageos/glimpse/viewmodels/AlbumViewerViewModel.kt
+++ b/app/src/main/java/org/lineageos/glimpse/viewmodels/AlbumViewerViewModel.kt
@@ -15,8 +15,10 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import org.lineageos.glimpse.ext.*
+import org.lineageos.glimpse.models.Album
import org.lineageos.glimpse.models.MediaStoreMedia
import org.lineageos.glimpse.recyclerview.ThumbnailAdapter
import org.lineageos.glimpse.repository.MediaRepository
@@ -71,6 +73,14 @@
initialValue = QueryResult.Empty(),
)
+ val album = MediaRepository.album(context, bucketId).flowOn(Dispatchers.IO).mapNotNull {
+ it.firstOrNull()
+ }.stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = Album(-1, ""),
+ )
+
val inSelectionMode = MutableLiveData(false)
sealed class DataType(val viewType: Int) {