diff --git a/app/src/main/java/org/lineageos/glimpse/GlimpseApplication.kt b/app/src/main/java/org/lineageos/glimpse/GlimpseApplication.kt index b123af8..c488607 100644 --- a/app/src/main/java/org/lineageos/glimpse/GlimpseApplication.kt +++ b/app/src/main/java/org/lineageos/glimpse/GlimpseApplication.kt @@ -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() diff --git a/app/src/main/java/org/lineageos/glimpse/flow/AlbumsFlow.kt b/app/src/main/java/org/lineageos/glimpse/flow/AlbumsFlow.kt new file mode 100644 index 0000000..006720f --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/flow/AlbumsFlow.kt @@ -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 { _, data: Cursor? -> + if (!isActive) return@OnLoadCompleteListener + launch(Dispatchers.IO) { + val albums = mutableMapOf().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() } + } +} diff --git a/app/src/main/java/org/lineageos/glimpse/flow/MediaFlow.kt b/app/src/main/java/org/lineageos/glimpse/flow/MediaFlow.kt new file mode 100644 index 0000000..a73a2da --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/flow/MediaFlow.kt @@ -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 { _, data: Cursor? -> + if (!isActive) return@OnLoadCompleteListener + launch(Dispatchers.IO) { + val media = mutableListOf().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() } + } +} diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumFragment.kt index 44f4fd9..58a1449 100644 --- a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumFragment.kt +++ b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumFragment.kt @@ -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 { +class AlbumFragment : Fragment(R.layout.fragment_album) { + // View models + private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory } + // Views private val albumRecyclerView by getViewProperty(R.id.albumRecyclerView) private val appBarLayout by getViewProperty(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) { - thumbnailAdapter.changeCursor(null) - } - - override fun onLoadFinished(loader: Loader, 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" diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumsFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumsFragment.kt index 5182bd1..357d9a7 100644 --- a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumsFragment.kt +++ b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumsFragment.kt @@ -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 { +class AlbumsFragment : Fragment() { + // View models + private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory } + // Views private val albumsRecyclerView by getViewProperty(R.id.albumsRecyclerView) @@ -49,7 +46,6 @@ class AlbumsFragment : Fragment(), LoaderManager.LoaderCallbacks { } // 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 { 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) { - albumThumbnailAdapter.changeArray(null) - } - - override fun onLoadFinished(loader: Loader, data: Cursor?) { - // Google killed GROUP BY in Android 10, forget about it - - val albums = mutableMapOf() - - 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 { diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt index 20cd9b3..381595a 100644 --- a/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt +++ b/app/src/main/java/org/lineageos/glimpse/fragments/MediaViewerFragment.kt @@ -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 { +class MediaViewerFragment : Fragment(R.layout.fragment_media_viewer) { + // View models + private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory } + // Views private val adjustButton by getViewProperty(R.id.adjustButton) private val backButton by getViewProperty(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) { - mediaViewerAdapter.changeCursor(null) - } - - override fun onLoadFinished(loader: Loader, 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) { + mediaViewerAdapter.data = data.toTypedArray() + viewPager.setCurrentItem(mediaViewModel.mediaPosition, false) + onPageChangeCallback.onPageSelected(mediaViewModel.mediaPosition) } private fun trashMedia(media: Media, trash: Boolean = !media.isTrashed) { diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/ReelsFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/ReelsFragment.kt index 3618065..7f67801 100644 --- a/app/src/main/java/org/lineageos/glimpse/fragments/ReelsFragment.kt +++ b/app/src/main/java/org/lineageos/glimpse/fragments/ReelsFragment.kt @@ -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 { +class ReelsFragment : Fragment(R.layout.fragment_reels) { + // View models + private val mediaViewModel: MediaViewModel by viewModels { MediaViewModel.Factory } + // Views private val reelsRecyclerView by getViewProperty(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) { - thumbnailAdapter.changeCursor(null) - } - - override fun onLoadFinished(loader: Loader, 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() diff --git a/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt b/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt new file mode 100644 index 0000000..a922510 --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/repository/MediaRepository.kt @@ -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() +} diff --git a/app/src/main/java/org/lineageos/glimpse/thumbnail/BaseCursorAdapter.kt b/app/src/main/java/org/lineageos/glimpse/thumbnail/BaseCursorAdapter.kt deleted file mode 100644 index 6556eb0..0000000 --- a/app/src/main/java/org/lineageos/glimpse/thumbnail/BaseCursorAdapter.kt +++ /dev/null @@ -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 : RecyclerView.Adapter() { - 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() - } -} diff --git a/app/src/main/java/org/lineageos/glimpse/thumbnail/MediaViewerAdapter.kt b/app/src/main/java/org/lineageos/glimpse/thumbnail/MediaViewerAdapter.kt index 61c6213..693f059 100644 --- a/app/src/main/java/org/lineageos/glimpse/thumbnail/MediaViewerAdapter.kt +++ b/app/src/main/java/org/lineageos/glimpse/thumbnail/MediaViewerAdapter.kt @@ -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, -) : BaseCursorAdapter() { - // 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() { + var data: Array = 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, diff --git a/app/src/main/java/org/lineageos/glimpse/thumbnail/ThumbnailAdapter.kt b/app/src/main/java/org/lineageos/glimpse/thumbnail/ThumbnailAdapter.kt index 9e598d0..5f98970 100644 --- a/app/src/main/java/org/lineageos/glimpse/thumbnail/ThumbnailAdapter.kt +++ b/app/src/main/java/org/lineageos/glimpse/thumbnail/ThumbnailAdapter.kt @@ -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.Adapter() { private val headersPositions = sortedSetOf() - // 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 = 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, diff --git a/app/src/main/java/org/lineageos/glimpse/viewmodels/MediaViewModel.kt b/app/src/main/java/org/lineageos/glimpse/viewmodels/MediaViewModel.kt new file mode 100644 index 0000000..95a5827 --- /dev/null +++ b/app/src/main/java/org/lineageos/glimpse/viewmodels/MediaViewModel.kt @@ -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(MEDIA_POSITION_KEY) + val mediaPositionLiveData: LiveData = 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(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, + ) + } + } + } +}