Glimpse: Implement data loading using the Repository/ViewModel pattern

Change-Id: Ia152000673b36e7f690ac5f2665ef1a0204bb339
This commit is contained in:
Luca Stefani 2023-08-08 22:48:04 +02:00
parent 72e7b9b16f
commit af3e76b1c3
12 changed files with 456 additions and 524 deletions

View File

@ -13,8 +13,11 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.VideoFrameDecoder
import com.google.android.material.color.DynamicColors
import org.lineageos.glimpse.repository.MediaRepository
class GlimpseApplication : Application(), ImageLoaderFactory {
val mediaRepository = MediaRepository(this)
override fun onCreate() {
super.onCreate()

View File

@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.glimpse.flow
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.os.Build
import android.provider.MediaStore
import androidx.core.os.bundleOf
import androidx.loader.content.GlimpseCursorLoader
import androidx.loader.content.Loader.OnLoadCompleteListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.lineageos.glimpse.R
import org.lineageos.glimpse.models.Album
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.utils.MediaStoreBuckets
import org.lineageos.glimpse.utils.MediaStoreRequests
class AlbumsFlow(private val context: Context) {
fun flow() = callbackFlow {
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 sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
val loader = GlimpseCursorLoader(
context,
MediaStore.Files.getContentUri("external"),
projection,
imageOrVideo.build(),
null,
sortOrder,
bundleOf().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
}
})
val onLoadCompleteListener = OnLoadCompleteListener<Cursor> { _, data: Cursor? ->
if (!isActive) return@OnLoadCompleteListener
launch(Dispatchers.IO) {
val albums = mutableMapOf<Int, Album>().apply {
data?.let {
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
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 bucketIdIndex =
it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
val bucketDisplayNameIndex =
it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
it.moveToFirst()
while (!it.isAfterLast) {
val contentUri = when (it.getInt(mediaTypeIndex)) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE ->
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO ->
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else -> continue
}
val bucketIds = listOfNotNull(
when (it.getInt(isTrashedIndex)) {
1 -> MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id
else -> it.getInt(bucketIdIndex)
},
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id.takeIf { _ ->
it.getInt(isFavoriteIndex) == 1
},
)
for (bucketId in bucketIds) {
this[bucketId]?.also { album ->
album.size += 1
} ?: run {
this[bucketId] = 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)
else ->
it.getString(bucketDisplayNameIndex) ?: Build.MODEL
},
ContentUris.withAppendedId(contentUri, it.getLong(idIndex)),
).apply { size += 1 }
}
}
it.moveToNext()
}
}
}.values.toList()
send(albums)
}
}
loader.registerListener(
MediaStoreRequests.MEDIA_STORE_ALBUMS_LOADER_ID.ordinal,
onLoadCompleteListener
)
launch(Dispatchers.IO) {
loader.startLoading()
}
awaitClose { loader.stopLoading() }
}
}

View File

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.glimpse.flow
import android.content.Context
import android.database.Cursor
import android.os.Build
import android.provider.MediaStore
import androidx.core.os.bundleOf
import androidx.loader.content.GlimpseCursorLoader
import androidx.loader.content.Loader.OnLoadCompleteListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.lineageos.glimpse.models.Media
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.utils.MediaStoreBuckets
import org.lineageos.glimpse.utils.MediaStoreRequests
class MediaFlow(private val context: Context, private val bucketId: Int?) {
fun flow() = callbackFlow {
val projection = MediaQuery.MediaProjection
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 = bucketId?.let {
when (it) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id ->
MediaStore.Files.FileColumns.IS_FAVORITE eq 1
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
MediaStore.Files.FileColumns.IS_TRASHED eq 1
} else {
null
}
else -> MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
}
}
val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
val loader = GlimpseCursorLoader(
context,
MediaStore.Files.getContentUri("external"),
projection,
selection.build(),
bucketId?.takeIf {
MediaStoreBuckets.values().none { bucket -> it == bucket.id }
}?.let { arrayOf(it.toString()) },
sortOrder,
bundleOf().apply {
// Exclude trashed media unless we want data for the trashed album
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putInt(
MediaStore.QUERY_ARG_MATCH_TRASHED,
when (bucketId) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
else -> MediaStore.MATCH_EXCLUDE
}
)
}
})
val onLoadCompleteListener = OnLoadCompleteListener<Cursor> { _, data: Cursor? ->
if (!isActive) return@OnLoadCompleteListener
launch(Dispatchers.IO) {
val media = mutableListOf<Media>().apply {
data?.let {
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
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 bucketIdIndex = it.getColumnIndex(MediaStore.MediaColumns.BUCKET_ID)
it.moveToFirst()
while (!it.isAfterLast) {
val id = it.getLong(idIndex)
val buckedId = it.getInt(bucketIdIndex)
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)
add(
Media.fromMediaStore(
id,
buckedId,
isFavorite,
isTrashed,
mediaType,
mimeType,
dateAdded,
)
)
it.moveToNext()
}
}
}.toList()
send(media)
}
}
loader.registerListener(
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
onLoadCompleteListener
)
launch(Dispatchers.IO) {
loader.startLoading()
}
awaitClose { loader.stopLoading() }
}
}

View File

@ -6,10 +6,7 @@
package org.lineageos.glimpse.fragments
import android.content.res.Configuration
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
@ -20,9 +17,8 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.GlimpseCursorLoader
import androidx.loader.content.Loader
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
@ -30,23 +26,26 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.shape.MaterialShapeDrawable
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.glimpse.R
import org.lineageos.glimpse.ext.getParcelable
import org.lineageos.glimpse.ext.getViewProperty
import org.lineageos.glimpse.models.Album
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.thumbnail.ThumbnailAdapter
import org.lineageos.glimpse.thumbnail.ThumbnailLayoutManager
import org.lineageos.glimpse.utils.MediaStoreBuckets
import org.lineageos.glimpse.utils.MediaStoreRequests
import org.lineageos.glimpse.utils.PermissionsUtils
import org.lineageos.glimpse.viewmodels.MediaViewModel
/**
* A fragment showing a list of media from a specific album with thumbnails.
* Use the [AlbumFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class AlbumFragment : Fragment(R.layout.fragment_album), LoaderManager.LoaderCallbacks<Cursor> {
class AlbumFragment : Fragment(R.layout.fragment_album) {
// View models
private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory }
// Views
private val albumRecyclerView by getViewProperty<RecyclerView>(R.id.albumRecyclerView)
private val appBarLayout by getViewProperty<AppBarLayout>(R.id.appBarLayout)
@ -64,13 +63,17 @@ class AlbumFragment : Fragment(R.layout.fragment_album), LoaderManager.LoaderCal
).show()
requireActivity().finish()
} else {
initCursorLoader()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.setBucketId(album.id)
mediaViewModel.mediaForAlbum.collectLatest { data ->
thumbnailAdapter.data = data.toTypedArray()
}
}
}
}
}
// MediaStore
private val loaderManagerInstance by lazy { LoaderManager.getInstance(this) }
private val thumbnailAdapter by lazy {
ThumbnailAdapter { media, position ->
findNavController().navigate(
@ -118,7 +121,12 @@ class AlbumFragment : Fragment(R.layout.fragment_album), LoaderManager.LoaderCal
if (!permissionsUtils.mainPermissionsGranted()) {
mainPermissionsRequestLauncher.launch(PermissionsUtils.mainPermissions)
} else {
initCursorLoader()
mediaViewModel.setBucketId(album.id)
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.mediaForAlbum.collectLatest { data ->
thumbnailAdapter.data = data.toTypedArray()
}
}
}
}
@ -130,71 +138,6 @@ class AlbumFragment : Fragment(R.layout.fragment_album), LoaderManager.LoaderCal
)
}
override fun onCreateLoader(id: Int, args: Bundle?) = when (id) {
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal -> {
val projection = MediaQuery.MediaProjection
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 (album.id) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> {
MediaStore.Files.FileColumns.IS_FAVORITE eq 1
}
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
MediaStore.Files.FileColumns.IS_TRASHED eq 1
} else {
null
}
}
else -> {
MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
}
}
val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
val queryArgs = args ?: Bundle()
GlimpseCursorLoader(
requireContext(),
MediaStore.Files.getContentUri("external"),
projection,
selection.build(),
album.takeIf {
MediaStoreBuckets.values().none { bucket -> it.id == bucket.id }
}?.let { arrayOf(it.id.toString()) },
sortOrder,
queryArgs
)
}
else -> throw Exception("Unknown ID $id")
}
override fun onLoaderReset(loader: Loader<Cursor>) {
thumbnailAdapter.changeCursor(null)
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
thumbnailAdapter.changeCursor(data)
}
private fun initCursorLoader() {
loaderManagerInstance.initLoader(
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
bundleOf().apply {
when (album.id) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}
}
}
}, this
)
}
companion object {
private const val KEY_ALBUM_ID = "album_id"

View File

@ -5,11 +5,7 @@
package org.lineageos.glimpse.fragments
import android.content.ContentUris
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -20,26 +16,27 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.GlimpseCursorLoader
import androidx.loader.content.Loader
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.glimpse.R
import org.lineageos.glimpse.ext.getViewProperty
import org.lineageos.glimpse.models.Album
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.thumbnail.AlbumThumbnailAdapter
import org.lineageos.glimpse.utils.MediaStoreBuckets
import org.lineageos.glimpse.utils.MediaStoreRequests
import org.lineageos.glimpse.viewmodels.MediaViewModel
/**
* An albums list visualizer.
* Use the [AlbumsFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class AlbumsFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
class AlbumsFragment : Fragment() {
// View models
private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory }
// Views
private val albumsRecyclerView by getViewProperty<RecyclerView>(R.id.albumsRecyclerView)
@ -49,7 +46,6 @@ class AlbumsFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
}
// MediaStore
private val loaderManagerInstance by lazy { LoaderManager.getInstance(this) }
private val albumThumbnailAdapter by lazy { AlbumThumbnailAdapter(parentNavController) }
override fun onCreateView(
@ -77,107 +73,11 @@ class AlbumsFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
windowInsets
}
loaderManagerInstance.initLoader(
MediaStoreRequests.MEDIA_STORE_ALBUMS_LOADER_ID.ordinal,
bundleOf().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
}
},
this
)
}
override fun onCreateLoader(id: Int, args: Bundle?) = when (id) {
MediaStoreRequests.MEDIA_STORE_ALBUMS_LOADER_ID.ordinal -> {
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 sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
val queryArgs = args ?: Bundle()
GlimpseCursorLoader(
requireContext(),
MediaStore.Files.getContentUri("external"),
projection,
imageOrVideo.build(),
null,
sortOrder,
queryArgs
)
}
else -> throw Exception("Unknown ID $id")
}
override fun onLoaderReset(loader: Loader<Cursor>) {
albumThumbnailAdapter.changeArray(null)
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
// Google killed GROUP BY in Android 10, forget about it
val albums = mutableMapOf<Int, Album>()
data?.let { cursor ->
if (cursor.count <= 0) {
return@let
}
val idIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)
val isFavoriteIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
val isTrashedIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
val mediaTypeIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
val bucketIdIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
val bucketDisplayNameIndex =
cursor.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val contentUri = when (cursor.getInt(mediaTypeIndex)) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE ->
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO ->
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else -> return@let
}
val bucketIds = listOfNotNull(
when (cursor.getInt(isTrashedIndex)) {
1 -> MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id
else -> cursor.getInt(bucketIdIndex)
},
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id.takeIf {
cursor.getInt(isFavoriteIndex) == 1
},
)
for (bucketId in bucketIds) {
albums[bucketId]?.also {
it.size += 1
} ?: run {
albums[bucketId] = Album(
bucketId,
when (bucketId) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id ->
getString(R.string.album_favorites)
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
getString(R.string.album_trash)
else -> cursor.getString(bucketDisplayNameIndex) ?: Build.MODEL
},
ContentUris.withAppendedId(contentUri, cursor.getLong(idIndex)),
).apply { size += 1 }
}
}
cursor.moveToNext()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.albums.collectLatest {
albumThumbnailAdapter.changeArray(it.toTypedArray())
}
}
albumThumbnailAdapter.changeArray(albums.values.toTypedArray())
}
companion object {

View File

@ -7,10 +7,8 @@ package org.lineageos.glimpse.fragments
import android.app.Activity
import android.content.Intent
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
@ -24,10 +22,8 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.loader.app.LoaderManager
import androidx.loader.content.GlimpseCursorLoader
import androidx.loader.content.Loader
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
@ -36,16 +32,16 @@ import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.glimpse.R
import org.lineageos.glimpse.ext.*
import org.lineageos.glimpse.models.Album
import org.lineageos.glimpse.models.Media
import org.lineageos.glimpse.models.MediaType
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.thumbnail.MediaViewerAdapter
import org.lineageos.glimpse.utils.MediaStoreBuckets
import org.lineageos.glimpse.utils.MediaStoreRequests
import org.lineageos.glimpse.utils.PermissionsUtils
import org.lineageos.glimpse.viewmodels.MediaViewModel
import java.text.SimpleDateFormat
/**
@ -53,9 +49,10 @@ import java.text.SimpleDateFormat
* Use the [MediaViewerFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class MediaViewerFragment : Fragment(
R.layout.fragment_media_viewer
), LoaderManager.LoaderCallbacks<Cursor> {
class MediaViewerFragment : Fragment(R.layout.fragment_media_viewer) {
// View models
private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory }
// Views
private val adjustButton by getViewProperty<ImageButton>(R.id.adjustButton)
private val backButton by getViewProperty<ImageButton>(R.id.backButton)
@ -82,7 +79,10 @@ class MediaViewerFragment : Fragment(
).show()
requireActivity().finish()
} else {
initCursorLoader()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.setBucketId(album?.id)
mediaViewModel.mediaForAlbum.collectLatest(::initData)
}
}
}
}
@ -96,19 +96,10 @@ class MediaViewerFragment : Fragment(
// Adapter
private val mediaViewerAdapter by lazy {
MediaViewerAdapter(exoPlayer, currentPositionLiveData)
MediaViewerAdapter(exoPlayer, mediaViewModel.mediaPositionLiveData)
}
// MediaStore
private val loaderManagerInstance by lazy { LoaderManager.getInstance(this) }
// Arguments
private val currentPositionLiveData = MutableLiveData(-1)
private var position: Int
get() = currentPositionLiveData.value!!
set(value) {
currentPositionLiveData.value = value
}
private val album by lazy { arguments?.getParcelable(KEY_ALBUM, Album::class) }
// Contracts
@ -167,7 +158,7 @@ class MediaViewerFragment : Fragment(
}
private val favoriteContract =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
favoriteButton.isSelected = it.isFavorite
}
}
@ -184,9 +175,9 @@ class MediaViewerFragment : Fragment(
return
}
this@MediaViewerFragment.position = position
this@MediaViewerFragment.mediaViewModel.mediaPosition = position
val media = mediaViewerAdapter.getMediaFromMediaStore(position) ?: return
val media = mediaViewerAdapter.getItemAtPosition(position)
dateTextView.text = dateFormatter.format(media.dateAdded)
timeTextView.text = timeFormatter.format(media.dateAdded)
@ -218,28 +209,24 @@ class MediaViewerFragment : Fragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
position = arguments?.getInt(KEY_POSITION, -1)!!
mediaViewModel.mediaPosition = arguments?.getInt(KEY_POSITION, -1)!!
backButton.setOnClickListener {
findNavController().popBackStack()
}
deleteButton.setOnClickListener {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
trashMedia(it)
}
trashMedia(mediaViewerAdapter.getItemAtPosition(viewPager.currentItem))
}
deleteButton.setOnLongClickListener {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.file_deletion_confirm_title)
mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.file_deletion_confirm_title)
.setMessage(
resources.getQuantityString(
R.plurals.file_deletion_confirm_message, 1, 1
)
)
.setPositiveButton(android.R.string.ok) { _, _ ->
).setPositiveButton(android.R.string.ok) { _, _ ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
deleteUriContract.launch(
requireContext().contentResolver.createDeleteRequest(
@ -262,7 +249,7 @@ class MediaViewerFragment : Fragment(
}
favoriteButton.setOnClickListener {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
favoriteContract.launch(
requireContext().contentResolver.createFavoriteRequest(
@ -298,14 +285,14 @@ class MediaViewerFragment : Fragment(
viewPager.registerOnPageChangeCallback(onPageChangeCallback)
shareButton.setOnClickListener {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
val intent = Intent().shareIntent(it)
startActivity(Intent.createChooser(intent, null))
}
}
adjustButton.setOnClickListener {
mediaViewerAdapter.getMediaFromMediaStore(viewPager.currentItem)?.let {
mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
val intent = Intent().editIntent(it)
startActivity(Intent.createChooser(intent, null))
}
@ -314,7 +301,10 @@ class MediaViewerFragment : Fragment(
if (!permissionsUtils.mainPermissionsGranted()) {
mainPermissionsRequestLauncher.launch(PermissionsUtils.mainPermissions)
} else {
initCursorLoader()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.setBucketId(album?.id)
mediaViewModel.mediaForAlbum.collectLatest(::initData)
}
}
}
@ -338,75 +328,10 @@ class MediaViewerFragment : Fragment(
super.onDestroy()
}
override fun onCreateLoader(id: Int, args: Bundle?) = when (id) {
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal -> {
val projection = MediaQuery.MediaProjection
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 = album?.let {
when (it.id) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> {
MediaStore.Files.FileColumns.IS_FAVORITE eq 1
}
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
MediaStore.Files.FileColumns.IS_TRASHED eq 1
} else {
null
}
}
else -> {
MediaStore.Files.FileColumns.BUCKET_ID eq Query.ARG
}
}
}
val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
val queryArgs = args ?: Bundle()
GlimpseCursorLoader(
requireContext(),
MediaStore.Files.getContentUri("external"),
projection,
selection.build(),
album?.takeIf {
MediaStoreBuckets.values().none { bucket -> it.id == bucket.id }
}?.let { arrayOf(it.id.toString()) },
sortOrder,
queryArgs
)
}
else -> throw Exception("Unknown ID $id")
}
override fun onLoaderReset(loader: Loader<Cursor>) {
mediaViewerAdapter.changeCursor(null)
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
mediaViewerAdapter.changeCursor(data)
viewPager.setCurrentItem(position, false)
onPageChangeCallback.onPageSelected(position)
}
private fun initCursorLoader() {
loaderManagerInstance.initLoader(
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
bundleOf().apply {
album?.let {
when (it.id) {
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}
}
}
}
}, this
)
private fun initData(data: List<Media>) {
mediaViewerAdapter.data = data.toTypedArray()
viewPager.setCurrentItem(mediaViewModel.mediaPosition, false)
onPageChangeCallback.onPageSelected(mediaViewModel.mediaPosition)
}
private fun trashMedia(media: Media, trash: Boolean = !media.isTrashed) {

View File

@ -6,10 +6,7 @@
package org.lineageos.glimpse.fragments
import android.content.res.Configuration
import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
@ -20,25 +17,28 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.glimpse.R
import org.lineageos.glimpse.ext.getViewProperty
import org.lineageos.glimpse.query.*
import org.lineageos.glimpse.thumbnail.ThumbnailAdapter
import org.lineageos.glimpse.thumbnail.ThumbnailLayoutManager
import org.lineageos.glimpse.utils.MediaStoreRequests
import org.lineageos.glimpse.utils.PermissionsUtils
import org.lineageos.glimpse.viewmodels.MediaViewModel
/**
* A fragment showing a list of media with thumbnails.
* Use the [ReelsFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class ReelsFragment : Fragment(R.layout.fragment_reels), LoaderManager.LoaderCallbacks<Cursor> {
class ReelsFragment : Fragment(R.layout.fragment_reels) {
// View models
private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory }
// Views
private val reelsRecyclerView by getViewProperty<RecyclerView>(R.id.reelsRecyclerView)
@ -59,14 +59,17 @@ class ReelsFragment : Fragment(R.layout.fragment_reels), LoaderManager.LoaderCal
).show()
requireActivity().finish()
} else {
initCursorLoader()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.media.collectLatest { data ->
thumbnailAdapter.data = data.toTypedArray()
}
}
permissionsUtils.showManageMediaPermissionDialogIfNeeded()
}
}
}
// MediaStore
private val loaderManagerInstance by lazy { LoaderManager.getInstance(this) }
private val thumbnailAdapter by lazy {
ThumbnailAdapter { media, position ->
parentNavController.navigate(
@ -101,7 +104,11 @@ class ReelsFragment : Fragment(R.layout.fragment_reels), LoaderManager.LoaderCal
if (!permissionsUtils.mainPermissionsGranted()) {
mainPermissionsRequestLauncher.launch(PermissionsUtils.mainPermissions)
} else {
initCursorLoader()
viewLifecycleOwner.lifecycleScope.launch {
mediaViewModel.media.collectLatest { data ->
thumbnailAdapter.data = data.toTypedArray()
}
}
permissionsUtils.showManageMediaPermissionDialogIfNeeded()
}
}
@ -114,53 +121,6 @@ class ReelsFragment : Fragment(R.layout.fragment_reels), LoaderManager.LoaderCal
)
}
override fun onCreateLoader(id: Int, args: Bundle?) = when (id) {
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal -> {
val projection = MediaQuery.MediaProjection
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 isNotTrashed = MediaStore.Files.FileColumns.IS_TRASHED eq 0
val selection = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// Exclude trashed medias
imageOrVideo and isNotTrashed
} else {
imageOrVideo
}
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
CursorLoader(
requireContext(),
MediaStore.Files.getContentUri("external"),
projection,
selection.build(),
null,
sortOrder
)
}
else -> throw Exception("Unknown ID $id")
}
override fun onLoaderReset(loader: Loader<Cursor>) {
thumbnailAdapter.changeCursor(null)
}
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
thumbnailAdapter.changeCursor(data)
}
private fun initCursorLoader() {
loaderManagerInstance.initLoader(
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
bundleOf().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Exclude trashed media
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_EXCLUDE)
}
},
this
)
}
companion object {
private fun createBundle() = bundleOf()

View File

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.glimpse.repository
import android.content.Context
import org.lineageos.glimpse.flow.AlbumsFlow
import org.lineageos.glimpse.flow.MediaFlow
class MediaRepository(private val context: Context) {
fun media(bucketId: Int? = null) = MediaFlow(context, bucketId).flow()
fun albums() = AlbumsFlow(context).flow()
}

View File

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.glimpse.thumbnail
import android.annotation.SuppressLint
import android.database.Cursor
import androidx.recyclerview.widget.RecyclerView
abstract class BaseCursorAdapter<T : RecyclerView.ViewHolder> : RecyclerView.Adapter<T>() {
protected var cursor: Cursor? = null
override fun getItemCount() = cursor?.count ?: 0
fun changeCursor(cursor: Cursor?) {
swapCursor(cursor)
onChangedCursor(cursor)
}
protected open fun onChangedCursor(cursor: Cursor?) {}
@SuppressLint("NotifyDataSetChanged")
private fun swapCursor(cursor: Cursor?) {
if (this.cursor == cursor) {
return
}
val oldCursor = this.cursor
this.cursor = cursor
cursor?.let {
notifyDataSetChanged()
}
oldCursor?.close()
}
}

View File

@ -5,8 +5,6 @@
package org.lineageos.glimpse.thumbnail
import android.database.Cursor
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -25,21 +23,27 @@ import org.lineageos.glimpse.models.MediaType
class MediaViewerAdapter(
private val exoPlayer: ExoPlayer,
private val currentPositionLiveData: LiveData<Int>,
) : BaseCursorAdapter<MediaViewerAdapter.MediaViewHolder>() {
// Cursor indexes
private var idIndex = -1
private var bucketIdIndex = -1
private var isFavoriteIndex = -1
private var isTrashedIndex = -1
private var mediaTypeIndex = -1
private var mimeTypeIndex = -1
private var dateAddedIndex = -1
) : RecyclerView.Adapter<MediaViewerAdapter.MediaViewHolder>() {
var data: Array<Media> = arrayOf()
set(value) {
if (value.contentEquals(field)) {
return
}
field = value
field.let {
@Suppress("NotifyDataSetChanged") notifyDataSetChanged()
}
}
init {
setHasStableIds(true)
}
override fun getItemId(position: Int) = getIdFromMediaStore(position)
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].id
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MediaViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.media_view, parent, false),
@ -47,7 +51,7 @@ class MediaViewerAdapter(
)
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
getMediaFromMediaStore(position)?.let { holder.bind(it, position) }
holder.bind(data[position], position)
}
override fun onViewAttachedToWindow(holder: MediaViewHolder) {
@ -60,49 +64,7 @@ class MediaViewerAdapter(
holder.onViewDetachedFromWindow()
}
override fun onChangedCursor(cursor: Cursor?) {
super.onChangedCursor(cursor)
cursor?.let {
idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
bucketIdIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
isFavoriteIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
isTrashedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
mediaTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
mimeTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)
dateAddedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED)
}
}
fun getMediaFromMediaStore(position: Int): Media? {
val cursor = cursor ?: return null
cursor.moveToPosition(position)
val id = cursor.getLong(idIndex)
val bucketId = cursor.getInt(bucketIdIndex)
val isFavorite = cursor.getInt(isFavoriteIndex)
val isTrashed = cursor.getInt(isTrashedIndex)
val mediaType = cursor.getInt(mediaTypeIndex)
val mimeType = cursor.getString(mimeTypeIndex)
val dateAdded = cursor.getLong(dateAddedIndex)
return Media.fromMediaStore(
id,
bucketId,
isFavorite,
isTrashed,
mediaType,
mimeType,
dateAdded,
)
}
private fun getIdFromMediaStore(position: Int): Long {
val cursor = cursor ?: return 0
cursor.moveToPosition(position)
return cursor.getLong(idIndex)
}
fun getItemAtPosition(position: Int) = data[position]
class MediaViewHolder(
private val view: View,

View File

@ -5,8 +5,6 @@
package org.lineageos.glimpse.thumbnail
import android.database.Cursor
import android.provider.MediaStore
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
@ -26,35 +24,40 @@ import java.util.Date
class ThumbnailAdapter(
private val onItemSelected: (media: Media, position: Int) -> Unit,
) : BaseCursorAdapter<RecyclerView.ViewHolder>() {
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val headersPositions = sortedSetOf<Int>()
// Cursor indexes
private var idIndex = -1
private var bucketIdIndex = -1
private var isFavoriteIndex = -1
private var isTrashedIndex = -1
private var mediaTypeIndex = -1
private var mimeTypeIndex = -1
private var dateAddedIndex = -1
var data: Array<Media> = arrayOf()
set(value) {
if (value.contentEquals(field)) {
return
}
field = value
field.let {
@Suppress("NotifyDataSetChanged") notifyDataSetChanged()
}
}
init {
setHasStableIds(true)
}
override fun getItemCount() =
super.getItemCount().takeIf { it > 0 }
?.let { it + (headersPositions.size.takeIf { headerCount -> headerCount > 0 } ?: 1) }
?: 0
override fun getItemCount() = data.size.takeIf { it > 0 }
?.let { it + (headersPositions.size.takeIf { headerCount -> headerCount > 0 } ?: 1) } ?: 0
override fun getItemId(position: Int) = getIdFromMediaStore(position)
override fun getItemId(position: Int) = if (headersPositions.contains(position)) {
position.toLong()
} else {
data[getTruePosition(position)].id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
LayoutInflater.from(parent.context).let { layoutInflater ->
when (viewType) {
ViewTypes.ITEM.ordinal -> ThumbnailViewHolder(
layoutInflater.inflate(R.layout.thumbnail_view, parent, false),
onItemSelected
layoutInflater.inflate(R.layout.thumbnail_view, parent, false), onItemSelected
)
ViewTypes.HEADER.ordinal -> DateHeaderViewHolder(
@ -71,16 +74,12 @@ class ThumbnailAdapter(
when (holder.itemViewType) {
ViewTypes.ITEM.ordinal -> {
val thumbnailViewHolder = holder as ThumbnailViewHolder
getMediaFromMediaStore(truePosition)?.let {
thumbnailViewHolder.bind(it, truePosition)
}
thumbnailViewHolder.bind(data[truePosition], truePosition)
}
ViewTypes.HEADER.ordinal -> {
val dateHeaderViewHolder = holder as DateHeaderViewHolder
getMediaFromMediaStore(truePosition)?.let {
dateHeaderViewHolder.bind(it.dateAdded)
}
dateHeaderViewHolder.bind(data[truePosition].dateAdded)
}
}
}
@ -106,8 +105,8 @@ class ThumbnailAdapter(
val truePosition = getTruePosition(position)
val previousTruePosition = truePosition - 1
val currentMedia = getMediaFromMediaStore(truePosition)!!
val previousMedia = getMediaFromMediaStore(previousTruePosition)!!
val currentMedia = data[truePosition]
val previousMedia = data[previousTruePosition]
val before = previousMedia.dateAdded.toInstant().atZone(ZoneId.systemDefault())
val after = currentMedia.dateAdded.toInstant().atZone(ZoneId.systemDefault())
@ -121,22 +120,6 @@ class ThumbnailAdapter(
return ViewTypes.ITEM.ordinal
}
override fun onChangedCursor(cursor: Cursor?) {
super.onChangedCursor(cursor)
headersPositions.clear()
cursor?.let {
idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
bucketIdIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
isFavoriteIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
isTrashedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
mediaTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
mimeTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)
dateAddedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED)
}
}
private fun getTruePosition(position: Int) =
position - headersPositions.filter { it < position }.size
@ -144,36 +127,6 @@ class ThumbnailAdapter(
headersPositions.add(position)
}
private fun getIdFromMediaStore(position: Int): Long {
val cursor = cursor ?: return 0
cursor.moveToPosition(getTruePosition(position))
return cursor.getLong(idIndex)
}
private fun getMediaFromMediaStore(position: Int): Media? {
val cursor = cursor ?: return null
cursor.moveToPosition(position)
val id = cursor.getLong(idIndex)
val bucketId = cursor.getInt(bucketIdIndex)
val isFavorite = cursor.getInt(isFavoriteIndex)
val isTrashed = cursor.getInt(isTrashedIndex)
val mediaType = cursor.getInt(mediaTypeIndex)
val mimeType = cursor.getString(mimeTypeIndex)
val dateAdded = cursor.getLong(dateAddedIndex)
return Media.fromMediaStore(
id,
bucketId,
isFavorite,
isTrashed,
mediaType,
mimeType,
dateAdded,
)
}
class ThumbnailViewHolder(
private val view: View,
private val onItemSelected: (media: Media, position: Int) -> Unit,

View File

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.glimpse.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import org.lineageos.glimpse.GlimpseApplication
import org.lineageos.glimpse.repository.MediaRepository
class MediaViewModel(
private val savedStateHandle: SavedStateHandle, private val mediaRepository: MediaRepository
) : ViewModel() {
private val mediaPositionInternal = savedStateHandle.getLiveData<Int>(MEDIA_POSITION_KEY)
val mediaPositionLiveData: LiveData<Int> = mediaPositionInternal
var mediaPosition: Int
set(value) {
mediaPositionInternal.value = value
}
get() = mediaPositionInternal.value!!
val media = mediaRepository.media(null)
val albums = mediaRepository.albums()
private val bucketId = MutableStateFlow<Int?>(null)
fun setBucketId(bucketId: Int?) {
this.bucketId.value = bucketId
}
@OptIn(ExperimentalCoroutinesApi::class)
val mediaForAlbum = bucketId.flatMapLatest { mediaRepository.media(it) }
companion object {
private const val MEDIA_POSITION_KEY = "position"
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
MediaViewModel(
savedStateHandle = createSavedStateHandle(),
mediaRepository = (this[APPLICATION_KEY] as GlimpseApplication).mediaRepository,
)
}
}
}
}