Glimpse: Don't use CursorLoader
Instead manually invoke the query and listen for updates on the external media Uri to trigger updates. Change-Id: I0d86484dfa453cce9910d2bcadee6191de5aed28
This commit is contained in:
parent
af3e76b1c3
commit
d4b1bfd6b2
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The LineageOS Project
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.loader.content
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.core.os.CancellationSignal
|
|
||||||
import androidx.core.os.OperationCanceledException
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom [CursorLoader] that uses the new [ContentResolver.query]'s
|
|
||||||
* queryArgs argument.
|
|
||||||
*/
|
|
||||||
class GlimpseCursorLoader(
|
|
||||||
context: Context,
|
|
||||||
uri: Uri,
|
|
||||||
projection: Array<out String>?,
|
|
||||||
selection: String?,
|
|
||||||
selectionArgs: Array<out String>?,
|
|
||||||
sortOrder: String?,
|
|
||||||
private val queryArgs: Bundle = Bundle()
|
|
||||||
) : CursorLoader(context, uri, projection, selection, selectionArgs, sortOrder) {
|
|
||||||
init {
|
|
||||||
queryArgs.putAll(
|
|
||||||
bundleOf(
|
|
||||||
ContentResolver.QUERY_ARG_SQL_SELECTION to selection,
|
|
||||||
ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
|
|
||||||
ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadInBackground(): Cursor? {
|
|
||||||
synchronized(this) {
|
|
||||||
if (isLoadInBackgroundCanceled) {
|
|
||||||
throw OperationCanceledException()
|
|
||||||
}
|
|
||||||
mCancellationSignal = CancellationSignal()
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
context.contentResolver.query(
|
|
||||||
mUri, mProjection, queryArgs,
|
|
||||||
mCancellationSignal.cancellationSignalObject as android.os.CancellationSignal?
|
|
||||||
)?.also {
|
|
||||||
try {
|
|
||||||
// Ensure the cursor window is filled.
|
|
||||||
it.count
|
|
||||||
it.registerContentObserver(mObserver)
|
|
||||||
} catch (ex: RuntimeException) {
|
|
||||||
it.close()
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
synchronized(this) { mCancellationSignal = null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,11 +6,19 @@
|
||||||
package org.lineageos.glimpse.ext
|
package org.lineageos.glimpse.ext
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.database.ContentObserver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.activity.result.IntentSenderRequest
|
import androidx.activity.result.IntentSenderRequest
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
fun ContentResolver.createDeleteRequest(vararg uris: Uri) = IntentSenderRequest.Builder(
|
fun ContentResolver.createDeleteRequest(vararg uris: Uri) = IntentSenderRequest.Builder(
|
||||||
|
@ -28,3 +36,29 @@ fun ContentResolver.createTrashRequest(value: Boolean, vararg uris: Uri) =
|
||||||
IntentSenderRequest.Builder(
|
IntentSenderRequest.Builder(
|
||||||
MediaStore.createTrashRequest(this, uris.toCollection(ArrayList()), value)
|
MediaStore.createTrashRequest(this, uris.toCollection(ArrayList()), value)
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
|
fun ContentResolver.uriFlow(uri: Uri) = callbackFlow {
|
||||||
|
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
trySend(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerContentObserver(uri, true, observer)
|
||||||
|
|
||||||
|
trySend(Unit)
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
unregisterContentObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentResolver.queryFlow(
|
||||||
|
uri: Uri,
|
||||||
|
projection: Array<String>? = null,
|
||||||
|
queryArgs: Bundle? = Bundle(),
|
||||||
|
cancellationSignal: CancellationSignal? = null
|
||||||
|
) = uriFlow(uri).map {
|
||||||
|
query(
|
||||||
|
uri, projection, queryArgs, cancellationSignal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The LineageOS Project
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.lineageos.glimpse.ext
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
|
||||||
|
fun <T> Cursor?.mapEachRow(mapping: (Cursor) -> T) = this?.use {
|
||||||
|
if (!moveToFirst()) {
|
||||||
|
emptyList<T>()
|
||||||
|
}
|
||||||
|
val data = mutableListOf<T>()
|
||||||
|
while (!isAfterLast) {
|
||||||
|
val element = mapping(this)
|
||||||
|
data.add(element)
|
||||||
|
moveToNext()
|
||||||
|
}
|
||||||
|
data.toList()
|
||||||
|
} ?: emptyList()
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The LineageOS Project
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.lineageos.glimpse.ext
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
fun <T> Flow<Cursor?>.mapEachRow(mapping: (Cursor) -> T) = map { it.mapEachRow(mapping) }
|
|
@ -5,70 +5,70 @@
|
||||||
|
|
||||||
package org.lineageos.glimpse.flow
|
package org.lineageos.glimpse.flow
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.loader.content.GlimpseCursorLoader
|
import kotlinx.coroutines.flow.Flow
|
||||||
import androidx.loader.content.Loader.OnLoadCompleteListener
|
import kotlinx.coroutines.flow.map
|
||||||
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.R
|
||||||
|
import org.lineageos.glimpse.ext.queryFlow
|
||||||
import org.lineageos.glimpse.models.Album
|
import org.lineageos.glimpse.models.Album
|
||||||
import org.lineageos.glimpse.query.*
|
import org.lineageos.glimpse.query.*
|
||||||
import org.lineageos.glimpse.utils.MediaStoreBuckets
|
import org.lineageos.glimpse.utils.MediaStoreBuckets
|
||||||
import org.lineageos.glimpse.utils.MediaStoreRequests
|
|
||||||
|
|
||||||
class AlbumsFlow(private val context: Context) {
|
class AlbumsFlow(private val context: Context) : QueryFlow<Album>() {
|
||||||
fun flow() = callbackFlow {
|
override fun flowCursor(): Flow<Cursor?> {
|
||||||
|
val uri = MediaQuery.MediaStoreFileUri
|
||||||
val projection = MediaQuery.AlbumsProjection
|
val projection = MediaQuery.AlbumsProjection
|
||||||
val imageOrVideo =
|
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_IMAGE) or
|
||||||
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
|
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
|
||||||
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
|
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
|
||||||
val loader = GlimpseCursorLoader(
|
val queryArgs = Bundle().apply {
|
||||||
context,
|
putAll(
|
||||||
MediaStore.Files.getContentUri("external"),
|
bundleOf(
|
||||||
projection,
|
ContentResolver.QUERY_ARG_SQL_SELECTION to imageOrVideo.build(),
|
||||||
imageOrVideo.build(),
|
ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
|
||||||
null,
|
)
|
||||||
sortOrder,
|
)
|
||||||
bundleOf().apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
|
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
val onLoadCompleteListener = OnLoadCompleteListener<Cursor> { _, data: Cursor? ->
|
return context.contentResolver.queryFlow(
|
||||||
if (!isActive) return@OnLoadCompleteListener
|
uri,
|
||||||
launch(Dispatchers.IO) {
|
projection,
|
||||||
val albums = mutableMapOf<Int, Album>().apply {
|
queryArgs,
|
||||||
data?.let {
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flowData() = flowCursor().map {
|
||||||
|
mutableMapOf<Int, Album>().apply {
|
||||||
|
it?.use {
|
||||||
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
|
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
|
||||||
val isFavoriteIndex =
|
val isFavoriteIndex =
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
|
it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
|
||||||
val isTrashedIndex =
|
val isTrashedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
|
val mediaTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
val mediaTypeIndex =
|
val bucketIdIndex = it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
|
||||||
val bucketIdIndex =
|
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_ID)
|
|
||||||
val bucketDisplayNameIndex =
|
val bucketDisplayNameIndex =
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
|
it.getColumnIndex(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
|
||||||
|
|
||||||
it.moveToFirst()
|
if (!it.moveToFirst()) {
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
|
||||||
while (!it.isAfterLast) {
|
while (!it.isAfterLast) {
|
||||||
val contentUri = when (it.getInt(mediaTypeIndex)) {
|
val contentUri = when (it.getInt(mediaTypeIndex)) {
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE ->
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
||||||
|
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO ->
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
||||||
|
|
||||||
else -> continue
|
else -> continue
|
||||||
}
|
}
|
||||||
|
@ -90,14 +90,15 @@ class AlbumsFlow(private val context: Context) {
|
||||||
this[bucketId] = Album(
|
this[bucketId] = Album(
|
||||||
bucketId,
|
bucketId,
|
||||||
when (bucketId) {
|
when (bucketId) {
|
||||||
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id ->
|
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> context.getString(
|
||||||
context.getString(R.string.album_favorites)
|
R.string.album_favorites
|
||||||
|
)
|
||||||
|
|
||||||
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
|
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> context.getString(
|
||||||
context.getString(R.string.album_trash)
|
R.string.album_trash
|
||||||
|
)
|
||||||
|
|
||||||
else ->
|
else -> it.getString(bucketDisplayNameIndex) ?: Build.MODEL
|
||||||
it.getString(bucketDisplayNameIndex) ?: Build.MODEL
|
|
||||||
},
|
},
|
||||||
ContentUris.withAppendedId(contentUri, it.getLong(idIndex)),
|
ContentUris.withAppendedId(contentUri, it.getLong(idIndex)),
|
||||||
).apply { size += 1 }
|
).apply { size += 1 }
|
||||||
|
@ -107,19 +108,5 @@ class AlbumsFlow(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.values.toList()
|
}.values.toList()
|
||||||
|
|
||||||
send(albums)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.registerListener(
|
|
||||||
MediaStoreRequests.MEDIA_STORE_ALBUMS_LOADER_ID.ordinal,
|
|
||||||
onLoadCompleteListener
|
|
||||||
)
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
loader.startLoading()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose { loader.stopLoading() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,36 +5,32 @@
|
||||||
|
|
||||||
package org.lineageos.glimpse.flow
|
package org.lineageos.glimpse.flow
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.loader.content.GlimpseCursorLoader
|
import kotlinx.coroutines.flow.Flow
|
||||||
import androidx.loader.content.Loader.OnLoadCompleteListener
|
import org.lineageos.glimpse.ext.mapEachRow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.lineageos.glimpse.ext.queryFlow
|
||||||
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.models.Media
|
||||||
import org.lineageos.glimpse.query.*
|
import org.lineageos.glimpse.query.*
|
||||||
import org.lineageos.glimpse.utils.MediaStoreBuckets
|
import org.lineageos.glimpse.utils.MediaStoreBuckets
|
||||||
import org.lineageos.glimpse.utils.MediaStoreRequests
|
|
||||||
|
|
||||||
class MediaFlow(private val context: Context, private val bucketId: Int?) {
|
class MediaFlow(private val context: Context, private val bucketId: Int?) : QueryFlow<Media>() {
|
||||||
fun flow() = callbackFlow {
|
override fun flowCursor(): Flow<Cursor?> {
|
||||||
|
val uri = MediaQuery.MediaStoreFileUri
|
||||||
val projection = MediaQuery.MediaProjection
|
val projection = MediaQuery.MediaProjection
|
||||||
val imageOrVideo =
|
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_IMAGE) or
|
||||||
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
|
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
|
||||||
val albumFilter = bucketId?.let {
|
val albumFilter = bucketId?.let {
|
||||||
when (it) {
|
when (it) {
|
||||||
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id ->
|
MediaStoreBuckets.MEDIA_STORE_BUCKET_FAVORITES.id -> MediaStore.Files.FileColumns.IS_FAVORITE eq 1
|
||||||
MediaStore.Files.FileColumns.IS_FAVORITE eq 1
|
|
||||||
|
|
||||||
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id ->
|
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
MediaStore.Files.FileColumns.IS_TRASHED eq 1
|
MediaStore.Files.FileColumns.IS_TRASHED eq 1
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -44,50 +40,47 @@ class MediaFlow(private val context: Context, private val bucketId: Int?) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
|
val selection = albumFilter?.let { imageOrVideo and it } ?: imageOrVideo
|
||||||
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
|
val selectionArgs = bucketId?.takeIf {
|
||||||
val loader = GlimpseCursorLoader(
|
|
||||||
context,
|
|
||||||
MediaStore.Files.getContentUri("external"),
|
|
||||||
projection,
|
|
||||||
selection.build(),
|
|
||||||
bucketId?.takeIf {
|
|
||||||
MediaStoreBuckets.values().none { bucket -> it == bucket.id }
|
MediaStoreBuckets.values().none { bucket -> it == bucket.id }
|
||||||
}?.let { arrayOf(it.toString()) },
|
}?.let { arrayOf(it.toString()) }
|
||||||
sortOrder,
|
val sortOrder = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
|
||||||
bundleOf().apply {
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
// Exclude trashed media unless we want data for the trashed album
|
// Exclude trashed media unless we want data for the trashed album
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
putInt(
|
putInt(
|
||||||
MediaStore.QUERY_ARG_MATCH_TRASHED,
|
MediaStore.QUERY_ARG_MATCH_TRASHED, when (bucketId) {
|
||||||
when (bucketId) {
|
|
||||||
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
|
MediaStoreBuckets.MEDIA_STORE_BUCKET_TRASH.id -> MediaStore.MATCH_ONLY
|
||||||
|
|
||||||
else -> MediaStore.MATCH_EXCLUDE
|
else -> MediaStore.MATCH_EXCLUDE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
val onLoadCompleteListener = OnLoadCompleteListener<Cursor> { _, data: Cursor? ->
|
return context.contentResolver.queryFlow(
|
||||||
if (!isActive) return@OnLoadCompleteListener
|
uri,
|
||||||
launch(Dispatchers.IO) {
|
projection,
|
||||||
val media = mutableListOf<Media>().apply {
|
queryArgs,
|
||||||
data?.let {
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flowData() = flowCursor().mapEachRow {
|
||||||
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
|
val idIndex = it.getColumnIndex(MediaStore.Files.FileColumns._ID)
|
||||||
val isFavoriteIndex =
|
val isFavoriteIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.IS_FAVORITE)
|
val isTrashedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
|
||||||
val isTrashedIndex =
|
val mediaTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
it.getColumnIndex(MediaStore.Files.FileColumns.IS_TRASHED)
|
val mimeTypeIndex = it.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)
|
||||||
val mediaTypeIndex =
|
val dateAddedIndex = it.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED)
|
||||||
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)
|
val bucketIdIndex = it.getColumnIndex(MediaStore.MediaColumns.BUCKET_ID)
|
||||||
|
|
||||||
it.moveToFirst()
|
|
||||||
while (!it.isAfterLast) {
|
|
||||||
val id = it.getLong(idIndex)
|
val id = it.getLong(idIndex)
|
||||||
val buckedId = it.getInt(bucketIdIndex)
|
val buckedId = it.getInt(bucketIdIndex)
|
||||||
val isFavorite = it.getInt(isFavoriteIndex)
|
val isFavorite = it.getInt(isFavoriteIndex)
|
||||||
|
@ -96,7 +89,6 @@ class MediaFlow(private val context: Context, private val bucketId: Int?) {
|
||||||
val mimeType = it.getString(mimeTypeIndex)
|
val mimeType = it.getString(mimeTypeIndex)
|
||||||
val dateAdded = it.getLong(dateAddedIndex)
|
val dateAdded = it.getLong(dateAddedIndex)
|
||||||
|
|
||||||
add(
|
|
||||||
Media.fromMediaStore(
|
Media.fromMediaStore(
|
||||||
id,
|
id,
|
||||||
buckedId,
|
buckedId,
|
||||||
|
@ -106,24 +98,5 @@ class MediaFlow(private val context: Context, private val bucketId: Int?) {
|
||||||
mimeType,
|
mimeType,
|
||||||
dateAdded,
|
dateAdded,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
it.moveToNext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
send(media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.registerListener(
|
|
||||||
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
|
|
||||||
onLoadCompleteListener
|
|
||||||
)
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
loader.startLoading()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose { loader.stopLoading() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The LineageOS Project
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.lineageos.glimpse.flow
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
abstract class QueryFlow<T> {
|
||||||
|
|
||||||
|
/** A flow of the data specified by the query */
|
||||||
|
abstract fun flowData(): Flow<List<T>>
|
||||||
|
|
||||||
|
/** A flow of the cursor specified by the query */
|
||||||
|
abstract fun flowCursor(): Flow<Cursor?>
|
||||||
|
}
|
|
@ -5,9 +5,11 @@
|
||||||
|
|
||||||
package org.lineageos.glimpse.query
|
package org.lineageos.glimpse.query
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
|
||||||
object MediaQuery {
|
object MediaQuery {
|
||||||
|
val MediaStoreFileUri: Uri = MediaStore.Files.getContentUri("external")
|
||||||
val MediaProjection = arrayOf(
|
val MediaProjection = arrayOf(
|
||||||
MediaStore.Files.FileColumns._ID,
|
MediaStore.Files.FileColumns._ID,
|
||||||
MediaStore.Files.FileColumns.BUCKET_ID,
|
MediaStore.Files.FileColumns.BUCKET_ID,
|
||||||
|
|
|
@ -9,7 +9,10 @@ import android.content.Context
|
||||||
import org.lineageos.glimpse.flow.AlbumsFlow
|
import org.lineageos.glimpse.flow.AlbumsFlow
|
||||||
import org.lineageos.glimpse.flow.MediaFlow
|
import org.lineageos.glimpse.flow.MediaFlow
|
||||||
|
|
||||||
|
@Suppress("Unused")
|
||||||
class MediaRepository(private val context: Context) {
|
class MediaRepository(private val context: Context) {
|
||||||
fun media(bucketId: Int? = null) = MediaFlow(context, bucketId).flow()
|
fun media(bucketId: Int? = null) = MediaFlow(context, bucketId).flowData()
|
||||||
fun albums() = AlbumsFlow(context).flow()
|
fun mediaCursor(bucketId: Int? = null) = MediaFlow(context, bucketId).flowCursor()
|
||||||
|
fun albums() = AlbumsFlow(context).flowData()
|
||||||
|
fun albumsCursor() = AlbumsFlow(context).flowCursor()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue