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
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
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 androidx.activity.result.IntentSenderRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun ContentResolver.createDeleteRequest(vararg uris: Uri) = IntentSenderRequest.Builder(
|
||||
|
@ -28,3 +36,29 @@ fun ContentResolver.createTrashRequest(value: Boolean, vararg uris: Uri) =
|
|||
IntentSenderRequest.Builder(
|
||||
MediaStore.createTrashRequest(this, uris.toCollection(ArrayList()), value)
|
||||
).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,121 +5,108 @@
|
|||
|
||||
package org.lineageos.glimpse.flow
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.os.bundleOf
|
||||
import 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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.lineageos.glimpse.R
|
||||
import org.lineageos.glimpse.ext.queryFlow
|
||||
import org.lineageos.glimpse.models.Album
|
||||
import org.lineageos.glimpse.query.*
|
||||
import org.lineageos.glimpse.utils.MediaStoreBuckets
|
||||
import org.lineageos.glimpse.utils.MediaStoreRequests
|
||||
|
||||
class AlbumsFlow(private val context: Context) {
|
||||
fun flow() = callbackFlow {
|
||||
class AlbumsFlow(private val context: Context) : QueryFlow<Album>() {
|
||||
override fun flowCursor(): Flow<Cursor?> {
|
||||
val uri = MediaQuery.MediaStoreFileUri
|
||||
val projection = MediaQuery.AlbumsProjection
|
||||
val imageOrVideo =
|
||||
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) or
|
||||
(MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
|
||||
val 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)
|
||||
val queryArgs = Bundle().apply {
|
||||
putAll(
|
||||
bundleOf(
|
||||
ContentResolver.QUERY_ARG_SQL_SELECTION to imageOrVideo.build(),
|
||||
ContentResolver.QUERY_ARG_SQL_SORT_ORDER to sortOrder,
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE)
|
||||
}
|
||||
}
|
||||
|
||||
loader.registerListener(
|
||||
MediaStoreRequests.MEDIA_STORE_ALBUMS_LOADER_ID.ordinal,
|
||||
onLoadCompleteListener
|
||||
return context.contentResolver.queryFlow(
|
||||
uri,
|
||||
projection,
|
||||
queryArgs,
|
||||
)
|
||||
launch(Dispatchers.IO) {
|
||||
loader.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose { loader.stopLoading() }
|
||||
override fun flowData() = flowCursor().map {
|
||||
mutableMapOf<Int, Album>().apply {
|
||||
it?.use {
|
||||
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)
|
||||
|
||||
if (!it.moveToFirst()) {
|
||||
return@use
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,125 +5,98 @@
|
|||
|
||||
package org.lineageos.glimpse.flow
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.os.bundleOf
|
||||
import 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 kotlinx.coroutines.flow.Flow
|
||||
import org.lineageos.glimpse.ext.mapEachRow
|
||||
import org.lineageos.glimpse.ext.queryFlow
|
||||
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 {
|
||||
class MediaFlow(private val context: Context, private val bucketId: Int?) : QueryFlow<Media>() {
|
||||
override fun flowCursor(): Flow<Cursor?> {
|
||||
val uri = MediaQuery.MediaStoreFileUri
|
||||
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_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
|
||||
}
|
||||
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 selectionArgs = bucketId?.takeIf {
|
||||
MediaStoreBuckets.values().none { bucket -> it == bucket.id }
|
||||
}?.let { arrayOf(it.toString()) }
|
||||
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
|
||||
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
|
||||
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()
|
||||
}
|
||||
else -> MediaStore.MATCH_EXCLUDE
|
||||
}
|
||||
}.toList()
|
||||
|
||||
send(media)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loader.registerListener(
|
||||
MediaStoreRequests.MEDIA_STORE_MEDIA_LOADER_ID.ordinal,
|
||||
onLoadCompleteListener
|
||||
return context.contentResolver.queryFlow(
|
||||
uri,
|
||||
projection,
|
||||
queryArgs,
|
||||
null,
|
||||
)
|
||||
launch(Dispatchers.IO) {
|
||||
loader.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose { loader.stopLoading() }
|
||||
override fun flowData() = flowCursor().mapEachRow {
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
Media.fromMediaStore(
|
||||
id,
|
||||
buckedId,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
mediaType,
|
||||
mimeType,
|
||||
dateAdded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
|
||||
object MediaQuery {
|
||||
val MediaStoreFileUri: Uri = MediaStore.Files.getContentUri("external")
|
||||
val MediaProjection = arrayOf(
|
||||
MediaStore.Files.FileColumns._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.MediaFlow
|
||||
|
||||
@Suppress("Unused")
|
||||
class MediaRepository(private val context: Context) {
|
||||
fun media(bucketId: Int? = null) = MediaFlow(context, bucketId).flow()
|
||||
fun albums() = AlbumsFlow(context).flow()
|
||||
fun media(bucketId: Int? = null) = MediaFlow(context, bucketId).flowData()
|
||||
fun mediaCursor(bucketId: Int? = null) = MediaFlow(context, bucketId).flowCursor()
|
||||
fun albums() = AlbumsFlow(context).flowData()
|
||||
fun albumsCursor() = AlbumsFlow(context).flowCursor()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue