#900 check media store changes on app resume
This commit is contained in:
parent
fcd2e493da
commit
f287dd4c04
6 changed files with 140 additions and 44 deletions
|
@ -3,6 +3,8 @@ package deckers.thibault.aves.channel.calls
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
|
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
@ -20,13 +22,15 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"checkObsoleteContentIds" -> ioScope.launch { safe(call, result, ::checkObsoleteContentIds) }
|
"checkObsoleteContentIds" -> ioScope.launch { safe(call, result, ::checkObsoleteContentIds) }
|
||||||
"checkObsoletePaths" -> ioScope.launch { safe(call, result, ::checkObsoletePaths) }
|
"checkObsoletePaths" -> ioScope.launch { safe(call, result, ::checkObsoletePaths) }
|
||||||
|
"getChangedUris" -> ioScope.launch { safe(call, result, ::getChangedUris) }
|
||||||
|
"getGeneration" -> ioScope.launch { safe(call, result, ::getGeneration) }
|
||||||
"scanFile" -> ioScope.launch { safe(call, result, ::scanFile) }
|
"scanFile" -> ioScope.launch { safe(call, result, ::scanFile) }
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) {
|
private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val knownContentIds = call.argument<List<Int?>>("knownContentIds")
|
val knownContentIds = call.argument<List<Number?>>("knownContentIds")?.map { it?.toLong() }
|
||||||
if (knownContentIds == null) {
|
if (knownContentIds == null) {
|
||||||
result.error("checkObsoleteContentIds-args", "missing arguments", null)
|
result.error("checkObsoleteContentIds-args", "missing arguments", null)
|
||||||
return
|
return
|
||||||
|
@ -35,7 +39,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) {
|
private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val knownPathById = call.argument<Map<Int?, String?>>("knownPathById")
|
val knownPathById = call.argument<Map<Number?, String?>>("knownPathById")?.mapKeys { it.key?.toLong() }
|
||||||
if (knownPathById == null) {
|
if (knownPathById == null) {
|
||||||
result.error("checkObsoletePaths-args", "missing arguments", null)
|
result.error("checkObsoletePaths-args", "missing arguments", null)
|
||||||
return
|
return
|
||||||
|
@ -43,6 +47,25 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById))
|
result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getChangedUris(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val sinceGeneration = call.argument<Int>("sinceGeneration")
|
||||||
|
if (sinceGeneration == null) {
|
||||||
|
result.error("getChangedUris-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val uris = MediaStoreImageProvider().getChangedUris(context, sinceGeneration)
|
||||||
|
result.success(uris)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGeneration(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val generation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
MediaStore.getGeneration(context, MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
result.success(generation)
|
||||||
|
}
|
||||||
|
|
||||||
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
|
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val path = call.argument<String>("path")
|
val path = call.argument<String>("path")
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
|
|
@ -19,13 +19,12 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
|
||||||
private lateinit var eventSink: EventSink
|
private lateinit var eventSink: EventSink
|
||||||
private lateinit var handler: Handler
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
private var knownEntries: Map<Int?, Int?>? = null
|
private var knownEntries: Map<Long?, Int?>? = null
|
||||||
private var directory: String? = null
|
private var directory: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (arguments is Map<*, *>) {
|
if (arguments is Map<*, *>) {
|
||||||
@Suppress("unchecked_cast")
|
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
|
||||||
knownEntries = arguments["knownEntries"] as Map<Int?, Int?>?
|
|
||||||
directory = arguments["directory"] as String?
|
directory = arguments["directory"] as String?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,11 @@ package deckers.thibault.aves.model.provider
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.RecoverableSecurityException
|
import android.app.RecoverableSecurityException
|
||||||
import android.content.*
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -35,7 +39,7 @@ import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.SyncFailedException
|
import java.io.SyncFailedException
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
@ -45,11 +49,11 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
class MediaStoreImageProvider : ImageProvider() {
|
class MediaStoreImageProvider : ImageProvider() {
|
||||||
fun fetchAll(
|
fun fetchAll(
|
||||||
context: Context,
|
context: Context,
|
||||||
knownEntries: Map<Int?, Int?>,
|
knownEntries: Map<Long?, Int?>,
|
||||||
directory: String?,
|
directory: String?,
|
||||||
handleNewEntry: NewEntryHandler,
|
handleNewEntry: NewEntryHandler,
|
||||||
) {
|
) {
|
||||||
val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean {
|
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
|
||||||
val knownDate = knownEntries[contentId]
|
val knownDate = knownEntries[contentId]
|
||||||
return knownDate == null || knownDate < dateModifiedSecs
|
return knownDate == null || knownDate < dateModifiedSecs
|
||||||
}
|
}
|
||||||
|
@ -89,7 +93,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
var found = false
|
var found = false
|
||||||
val fetched = arrayListOf<FieldMap>()
|
val fetched = arrayListOf<FieldMap>()
|
||||||
val id = uri.tryParseId()
|
val id = uri.tryParseId()
|
||||||
val alwaysValid: NewEntryChecker = fun(_: Int, _: Int): Boolean = true
|
val alwaysValid: NewEntryChecker = fun(_: Long, _: Int): Boolean = true
|
||||||
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
|
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
if (sourceMimeType == null || isImage(sourceMimeType)) {
|
if (sourceMimeType == null || isImage(sourceMimeType)) {
|
||||||
|
@ -119,8 +123,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkObsoleteContentIds(context: Context, knownContentIds: List<Int?>): List<Int> {
|
fun checkObsoleteContentIds(context: Context, knownContentIds: List<Long?>): List<Long> {
|
||||||
val foundContentIds = HashSet<Int>()
|
val foundContentIds = HashSet<Long>()
|
||||||
fun check(context: Context, contentUri: Uri) {
|
fun check(context: Context, contentUri: Uri) {
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
try {
|
try {
|
||||||
|
@ -128,7 +132,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
foundContentIds.add(cursor.getInt(idColumn))
|
foundContentIds.add(cursor.getLong(idColumn))
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
|
@ -141,8 +145,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return knownContentIds.subtract(foundContentIds).filterNotNull().toList()
|
return knownContentIds.subtract(foundContentIds).filterNotNull().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> {
|
fun checkObsoletePaths(context: Context, knownPathById: Map<Long?, String?>): List<Long> {
|
||||||
val obsoleteIds = ArrayList<Int>()
|
val obsoleteIds = ArrayList<Long>()
|
||||||
fun check(context: Context, contentUri: Uri) {
|
fun check(context: Context, contentUri: Uri) {
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||||
try {
|
try {
|
||||||
|
@ -151,7 +155,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getInt(idColumn)
|
val id = cursor.getLong(idColumn)
|
||||||
val path = cursor.getString(pathColumn)
|
val path = cursor.getString(pathColumn)
|
||||||
if (knownPathById.containsKey(id) && knownPathById[id] != path) {
|
if (knownPathById.containsKey(id) && knownPathById[id] != path) {
|
||||||
obsoleteIds.add(id)
|
obsoleteIds.add(id)
|
||||||
|
@ -168,6 +172,31 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return obsoleteIds
|
return obsoleteIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChangedUris(context: Context, sinceGeneration: Int): List<String> {
|
||||||
|
val changedUris = ArrayList<String>()
|
||||||
|
fun check(context: Context, contentUri: Uri) {
|
||||||
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
|
val selection = "${MediaStore.MediaColumns.GENERATION_MODIFIED} > ?"
|
||||||
|
val selectionArgs = arrayOf(sinceGeneration.toString())
|
||||||
|
try {
|
||||||
|
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null)
|
||||||
|
if (cursor != null) {
|
||||||
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(idColumn)
|
||||||
|
changedUris.add(ContentUris.withAppendedId(contentUri, id).toString())
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "failed to get content IDs for contentUri=$contentUri", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(context, IMAGE_CONTENT_URI)
|
||||||
|
check(context, VIDEO_CONTENT_URI)
|
||||||
|
return changedUris
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchFrom(
|
private fun fetchFrom(
|
||||||
context: Context,
|
context: Context,
|
||||||
isValidEntry: NewEntryChecker,
|
isValidEntry: NewEntryChecker,
|
||||||
|
@ -207,12 +236,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
val needDuration = projection.contentEquals(VIDEO_PROJECTION)
|
val needDuration = projection.contentEquals(VIDEO_PROJECTION)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val contentId = cursor.getInt(idColumn)
|
val id = cursor.getLong(idColumn)
|
||||||
val dateModifiedSecs = cursor.getInt(dateModifiedColumn)
|
val dateModifiedSecs = cursor.getInt(dateModifiedColumn)
|
||||||
if (isValidEntry(contentId, dateModifiedSecs)) {
|
if (isValidEntry(id, dateModifiedSecs)) {
|
||||||
// for multiple items, `contentUri` is the root without ID,
|
// for multiple items, `contentUri` is the root without ID,
|
||||||
// but for single items, `contentUri` already contains the ID
|
// but for single items, `contentUri` already contains the ID
|
||||||
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong())
|
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
|
||||||
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
|
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
|
||||||
// in that case we try to use the MIME type provided along the URI
|
// in that case we try to use the MIME type provided along the URI
|
||||||
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
|
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
|
||||||
|
@ -237,7 +266,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
|
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
|
||||||
"durationMillis" to durationMillis,
|
"durationMillis" to durationMillis,
|
||||||
// only for map export
|
// only for map export
|
||||||
"contentId" to contentId,
|
"contentId" to id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MimeTypes.isHeic(mimeType)) {
|
if (MimeTypes.isHeic(mimeType)) {
|
||||||
|
@ -930,8 +959,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
try {
|
try {
|
||||||
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null)
|
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null)
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
cursor.getColumnIndex(MediaStore.MediaColumns._ID).let {
|
val idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
|
||||||
if (it != -1) mediaContentUri = ContentUris.withAppendedId(contentUri, cursor.getLong(it))
|
if (idColumn != -1) {
|
||||||
|
val id = cursor.getLong(idColumn)
|
||||||
|
mediaContentUri = ContentUris.withAppendedId(contentUri, id)
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
|
@ -994,4 +1025,4 @@ object MediaColumns {
|
||||||
|
|
||||||
typealias NewEntryHandler = (entry: FieldMap) -> Unit
|
typealias NewEntryHandler = (entry: FieldMap) -> Unit
|
||||||
|
|
||||||
private typealias NewEntryChecker = (contentId: Int, dateModifiedSecs: Int) -> Boolean
|
private typealias NewEntryChecker = (contentId: Long, dateModifiedSecs: Int) -> Boolean
|
|
@ -11,12 +11,17 @@ import 'package:aves/model/source/analysis_controller.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class MediaStoreSource extends CollectionSource {
|
class MediaStoreSource extends CollectionSource {
|
||||||
|
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
|
||||||
|
final Set<String> _changedUris = {};
|
||||||
|
int? _lastGeneration;
|
||||||
SourceInitializationState _initState = SourceInitializationState.none;
|
SourceInitializationState _initState = SourceInitializationState.none;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,6 +41,7 @@ class MediaStoreSource extends CollectionSource {
|
||||||
_initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full;
|
_initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full;
|
||||||
}
|
}
|
||||||
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
|
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
|
||||||
|
await updateGeneration();
|
||||||
unawaited(_loadEntries(
|
unawaited(_loadEntries(
|
||||||
analysisController: analysisController,
|
analysisController: analysisController,
|
||||||
directory: directory,
|
directory: directory,
|
||||||
|
@ -305,6 +311,34 @@ class MediaStoreSource extends CollectionSource {
|
||||||
return tempUris;
|
return tempUris;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onStoreChanged(String? uri) {
|
||||||
|
if (uri != null) _changedUris.add(uri);
|
||||||
|
if (_changedUris.isNotEmpty) {
|
||||||
|
_changeDebouncer(() async {
|
||||||
|
final todo = _changedUris.toSet();
|
||||||
|
_changedUris.clear();
|
||||||
|
final tempUris = await refreshUris(todo);
|
||||||
|
if (tempUris.isNotEmpty) {
|
||||||
|
_changedUris.addAll(tempUris);
|
||||||
|
onStoreChanged(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkForChanges() async {
|
||||||
|
final sinceGeneration = _lastGeneration;
|
||||||
|
if (sinceGeneration != null) {
|
||||||
|
_changedUris.addAll(await mediaStoreService.getChangedUris(sinceGeneration));
|
||||||
|
onStoreChanged(null);
|
||||||
|
}
|
||||||
|
await updateGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateGeneration() async {
|
||||||
|
_lastGeneration = await mediaStoreService.getGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
// vault
|
// vault
|
||||||
|
|
||||||
Future<void> _loadVaultEntries(String? directory) async {
|
Future<void> _loadVaultEntries(String? directory) async {
|
||||||
|
|
|
@ -10,6 +10,10 @@ abstract class MediaStoreService {
|
||||||
|
|
||||||
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById);
|
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById);
|
||||||
|
|
||||||
|
Future<List<String>> getChangedUris(int sinceGeneration);
|
||||||
|
|
||||||
|
Future<int?> getGeneration();
|
||||||
|
|
||||||
// knownEntries: map of contentId -> dateModifiedSecs
|
// knownEntries: map of contentId -> dateModifiedSecs
|
||||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
|
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
|
||||||
|
|
||||||
|
@ -47,6 +51,29 @@ class PlatformMediaStoreService implements MediaStoreService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getChangedUris(int sinceGeneration) async {
|
||||||
|
try {
|
||||||
|
final result = await _platform.invokeMethod('getChangedUris', <String, dynamic>{
|
||||||
|
'sinceGeneration': sinceGeneration,
|
||||||
|
});
|
||||||
|
return (result as List).cast<String>();
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int?> getGeneration() async {
|
||||||
|
try {
|
||||||
|
return await _platform.invokeMethod('getGeneration');
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
|
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -19,11 +19,9 @@ import 'package:aves/model/source/media_store_source.dart';
|
||||||
import 'package:aves/services/accessibility_service.dart';
|
import 'package:aves/services/accessibility_service.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/styles.dart';
|
import 'package:aves/theme/styles.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
|
||||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
|
@ -154,9 +152,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
late final Future<void> _appSetup;
|
late final Future<void> _appSetup;
|
||||||
late final Future<bool> _shouldUseBoldFontLoader;
|
late final Future<bool> _shouldUseBoldFontLoader;
|
||||||
final TvRailController _tvRailController = TvRailController();
|
final TvRailController _tvRailController = TvRailController();
|
||||||
final CollectionSource _mediaStoreSource = MediaStoreSource();
|
final MediaStoreSource _mediaStoreSource = MediaStoreSource();
|
||||||
final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
|
|
||||||
final Set<String> _changedUris = {};
|
|
||||||
Size? _screenSize;
|
Size? _screenSize;
|
||||||
|
|
||||||
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
|
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
|
||||||
|
@ -184,7 +180,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
EquatableConfig.stringify = true;
|
EquatableConfig.stringify = true;
|
||||||
_appSetup = _setup();
|
_appSetup = _setup();
|
||||||
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
|
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
|
||||||
_subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?)));
|
_subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _mediaStoreSource.onStoreChanged(event as String?)));
|
||||||
_subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)));
|
_subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)));
|
||||||
_subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()));
|
_subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()));
|
||||||
_subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)));
|
_subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)));
|
||||||
|
@ -399,6 +395,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
RecentlyAddedFilter.updateNow();
|
RecentlyAddedFilter.updateNow();
|
||||||
|
_mediaStoreSource.checkForChanges();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -614,21 +611,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
_mediaStoreSource.updateDerivedFilters();
|
_mediaStoreSource.updateDerivedFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMediaStoreChanged(String? uri) {
|
|
||||||
if (uri != null) _changedUris.add(uri);
|
|
||||||
if (_changedUris.isNotEmpty) {
|
|
||||||
_mediaStoreChangeDebouncer(() async {
|
|
||||||
final todo = _changedUris.toSet();
|
|
||||||
_changedUris.clear();
|
|
||||||
final tempUris = await _mediaStoreSource.refreshUris(todo);
|
|
||||||
if (tempUris.isNotEmpty) {
|
|
||||||
_changedUris.addAll(tempUris);
|
|
||||||
_onMediaStoreChanged(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onError(String? error) => reportService.recordError(error, null);
|
void _onError(String? error) => reportService.recordError(error, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue