diff --git a/lib/main.dart b/lib/main.dart index 02cb28210..4cefeea66 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -190,10 +190,17 @@ class _AvesAppState extends State { } void _onContentChange(String uri) { - changedUris.add(uri); - _contentChangeDebouncer(() { - _mediaStoreSource.refreshUris(List.of(changedUris)); - changedUris.clear(); - }); + if (uri != null) changedUris.add(uri); + if (changedUris.isNotEmpty) { + _contentChangeDebouncer(() async { + final todo = List.of(changedUris); + changedUris.clear(); + final tempUris = await _mediaStoreSource.refreshUris(todo); + if (tempUris.isNotEmpty) { + changedUris.addAll(tempUris); + _onContentChange(null); + } + }); + } } } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index ea0cd40f2..44f24eae2 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -7,6 +7,7 @@ import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; @@ -105,8 +106,12 @@ class MediaStoreSource extends CollectionSource { ); } - Future refreshUris(List changedUris) async { - if (!_initialized) return; + // returns URIs that are in the Media Store but still being processed by their owner in a temporary location + // For example, when taking a picture with a Galaxy S10e default camera app, querying the Media Store + // sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` + Future> refreshUris(List changedUris) async { + final tempUris = []; + if (!_initialized) return tempUris; final uriByContentId = Map.fromEntries(changedUris.map((uri) { if (uri == null) return null; @@ -121,7 +126,7 @@ class MediaStoreSource extends CollectionSource { uriByContentId.removeWhere((contentId, _) => obsoleteContentIds.contains(contentId)); metadataDb.removeIds(obsoleteContentIds, updateFavourites: true); - // add new entries + // fetch new entries final newEntries = []; for (final kv in uriByContentId.entries) { final contentId = kv.key; @@ -129,20 +134,31 @@ class MediaStoreSource extends CollectionSource { final sourceEntry = await ImageFileService.getEntry(uri, null); final existingEntry = rawEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs) { - newEntries.add(sourceEntry); + final volume = androidFileUtils.getStorageVolume(sourceEntry.path); + if (volume != null) { + newEntries.add(sourceEntry); + } else { + debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...'); + tempUris.add(uri); + } } } - addAll(newEntries); - await metadataDb.saveEntries(newEntries); - updateAlbums(); - stateNotifier.value = SourceState.cataloguing; - await catalogEntries(); + if (newEntries.isNotEmpty) { + addAll(newEntries); + await metadataDb.saveEntries(newEntries); + updateAlbums(); - stateNotifier.value = SourceState.locating; - await locateEntries(); + stateNotifier.value = SourceState.cataloguing; + await catalogEntries(); - stateNotifier.value = SourceState.ready; + stateNotifier.value = SourceState.locating; + await locateEntries(); + + stateNotifier.value = SourceState.ready; + } + + return tempUris; } @override diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 9cc883fda..0c8827f16 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -48,11 +48,5 @@ class Durations { static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const searchDebounceDelay = Duration(milliseconds: 250); - - // Content change monitoring delay should be large enough, - // so that querying the Media Store yields final entries. - // For example, when taking a picture with a Galaxy S10e default camera app, - // querying the Media Store just 1 second after sometimes yields an entry with - // its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` - static const contentChangeDebounceDelay = Duration(milliseconds: 1500); + static const contentChangeDebounceDelay = Duration(milliseconds: 500); }