source: disable analysis for widget, screen saver; disabling analysis also disables entry discovery

This commit is contained in:
Thibault Deckers 2024-10-07 22:24:58 +02:00
parent 618b63bfc0
commit 211f803afe
17 changed files with 64 additions and 86 deletions

View file

@ -296,14 +296,11 @@ open class MainActivity : FlutterFragmentActivity() {
open fun extractIntentData(intent: Intent?): FieldMap {
when (val action = intent?.action) {
Intent.ACTION_MAIN -> {
val fields = HashMap<String, Any?>()
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
fields[INTENT_DATA_KEY_SAFE_MODE] = true
}
fields[INTENT_DATA_KEY_PAGE] = intent.getStringExtra(EXTRA_KEY_PAGE)
fields[INTENT_DATA_KEY_FILTERS] = extractFiltersFromIntent(intent)
fields[INTENT_DATA_KEY_EXPLORER_PATH] = intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH)
return fields
return hashMapOf(
INTENT_DATA_KEY_PAGE to intent.getStringExtra(EXTRA_KEY_PAGE),
INTENT_DATA_KEY_FILTERS to extractFiltersFromIntent(intent),
INTENT_DATA_KEY_EXPLORER_PATH to intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH),
)
}
Intent.ACTION_VIEW,
@ -557,7 +554,6 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SAFE_MODE = "safeMode"
const val INTENT_DATA_KEY_SECURE_URIS = "secureUris"
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
@ -566,7 +562,6 @@ open class MainActivity : FlutterFragmentActivity() {
const val EXTRA_KEY_EXPLORER_PATH = "explorerPath"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode"
const val EXTRA_KEY_WIDGET_ID = "widgetId"
// dart page routes

View file

@ -21,15 +21,11 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private var knownEntries: Map<Long?, Int?>? = null
private var directory: String? = null
private var safe: Boolean = false
init {
if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
directory = arguments["directory"] as String?
// do not use kotlin.collections `getOrDefault` as it crashes on API <24
// and there is no warning from Android Studio
safe = arguments["safe"] as Boolean? ?: false
}
}
@ -63,7 +59,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
}
private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) }
endOfStream()
}

View file

@ -5,7 +5,6 @@ import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory
@ -29,6 +28,7 @@ import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
class SourceEntry {
private val origin: Int
@ -116,8 +116,8 @@ class SourceEntry {
// metadata retrieval
// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry {
if (isSvg || safe) return this
fun fillPreCatalogMetadata(context: Context): SourceEntry {
if (isSvg) return this
if (isVideo) {
fillVideoByMediaMetadataRetriever(context)
if (isSized && hasDuration) return this

View file

@ -53,7 +53,7 @@ internal class FileImageProvider : ImageProvider() {
return
}
}
entry.fillPreCatalogMetadata(context, safe = false)
entry.fillPreCatalogMetadata(context)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())

View file

@ -51,10 +51,9 @@ class MediaStoreImageProvider : ImageProvider() {
context: Context,
knownEntries: Map<Long?, Int?>,
directory: String?,
safe: Boolean,
handleNewEntry: NewEntryHandler,
) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe")
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
@ -84,8 +83,8 @@ class MediaStoreImageProvider : ImageProvider() {
} else {
handleNew = handleNewEntry
}
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe)
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs)
}
// the provided URI can point to the wrong media collection,
@ -208,7 +207,6 @@ class MediaStoreImageProvider : ImageProvider() {
selection: String? = null,
selectionArgs: Array<String>? = null,
fileMimeType: String? = null,
safe: Boolean = false,
): Boolean {
var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
@ -302,7 +300,7 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
entryMap = entry.toMap()
}

View file

@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
return
}
val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {

View file

@ -21,7 +21,6 @@ class IntentDataKeys {
static const mimeType = 'mimeType';
static const page = 'page';
static const query = 'query';
static const safeMode = 'safeMode';
static const secureUris = 'secureUris';
static const uri = 'uri';
static const widgetId = 'widgetId';

View file

@ -31,7 +31,7 @@ import 'package:collection/collection.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
enum SourceInitializationState { none, directory, full }
enum SourceScope { none, album, full }
mixin SourceBase {
EventBus get eventBus;
@ -93,7 +93,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
_rawEntries.forEach((v) => v.dispose());
}
set safeMode(bool enabled);
set canAnalyze(bool enabled);
final EventBus _eventBus = EventBus();
@ -427,13 +427,12 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
eventBus.fire(EntryMovedEvent(MoveType.move, movedEntries));
}
SourceInitializationState get initState => SourceInitializationState.none;
SourceScope get scope => SourceScope.none;
Future<void> init({
AnalysisController? analysisController,
String? directory,
AlbumFilter? albumFilter,
bool loadTopEntriesFirst = false,
bool canAnalyze = true,
});
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
@ -518,13 +517,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
// monitoring
bool _monitoring = true;
bool _canRefresh = true;
void pauseMonitoring() => _monitoring = false;
void pauseMonitoring() => _canRefresh = false;
void resumeMonitoring() => _monitoring = true;
void resumeMonitoring() => _canRefresh = true;
bool get isMonitoring => _monitoring;
bool get canRefresh => _canRefresh;
// filter summary

View file

@ -21,36 +21,34 @@ class MediaStoreSource extends CollectionSource {
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
final Set<String> _changedUris = {};
int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none;
bool _safeMode = false;
SourceScope _scope = SourceScope.none;
bool _canAnalyze = true;
@override
set safeMode(bool enabled) => _safeMode = enabled;
set canAnalyze(bool enabled) => _canAnalyze = enabled;
@override
SourceInitializationState get initState => _initState;
SourceScope get scope => _scope;
@override
Future<void> init({
AnalysisController? analysisController,
String? directory,
AlbumFilter? albumFilter,
bool loadTopEntriesFirst = false,
bool canAnalyze = true,
}) async {
await reportService.log('$runtimeType init directory=$directory');
if (_initState == SourceInitializationState.none) {
await reportService.log('$runtimeType init album=${albumFilter?.album}');
if (_scope == SourceScope.none) {
await _loadEssentials();
}
if (_initState != SourceInitializationState.full) {
_initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full;
if (_scope != SourceScope.full) {
_scope = albumFilter != null ? SourceScope.album : SourceScope.full;
}
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
await updateGeneration();
unawaited(_loadEntries(
analysisController: analysisController,
directory: directory,
directory: albumFilter?.album,
loadTopEntriesFirst: loadTopEntriesFirst,
canAnalyze: canAnalyze && !_safeMode,
));
}
@ -80,7 +78,6 @@ class MediaStoreSource extends CollectionSource {
AnalysisController? analysisController,
String? directory,
required bool loadTopEntriesFirst,
required bool canAnalyze,
}) async {
unawaited(reportService.log('$runtimeType load start'));
final stopwatch = Stopwatch()..start();
@ -158,6 +155,12 @@ class MediaStoreSource extends CollectionSource {
knownDateByContentId[contentId] = 0;
});
if (!_canAnalyze) {
// it can discover new entries only if it can analyze them
state = SourceState.ready;
return;
}
// items to add to the collection
final newEntries = <AvesEntry>{};
@ -169,7 +172,7 @@ class MediaStoreSource extends CollectionSource {
// fetch new & modified entries
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.
@ -210,11 +213,7 @@ class MediaStoreSource extends CollectionSource {
if (analysisIds != null) {
analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
}
if (canAnalyze) {
await analyze(analysisController, entries: analysisEntries);
} else {
state = SourceState.ready;
}
// the home page may not reflect the current derived filters
// as the initial addition of entries is silent,
@ -234,7 +233,7 @@ class MediaStoreSource extends CollectionSource {
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (_initState == SourceInitializationState.none || !isMonitoring || !isReady) return changedUris;
if (_scope == SourceScope.none || !canRefresh || !isReady) return changedUris;
state = SourceState.loading;
@ -272,7 +271,8 @@ class MediaStoreSource extends CollectionSource {
if (volume != null) {
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
} else {
} else if (_canAnalyze) {
// it can discover new entries only if it can analyze them
sourceEntry.id = localMediaDb.nextId;
newEntries.add(sourceEntry);
}
@ -329,10 +329,6 @@ class MediaStoreSource extends CollectionSource {
}
void onStoreChanged(String? uri) {
// dismiss changes if the source is only loaded to view a specific directory
// to let the main instance handle the change in the database
if (_initState == SourceInitializationState.directory) return;
if (uri != null) _changedUris.add(uri);
if (_changedUris.isNotEmpty) {
_changeDebouncer(() async {

View file

@ -15,7 +15,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory});
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
@ -77,13 +77,12 @@ class PlatformMediaStoreService implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) {
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
try {
return _stream
.receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries,
'directory': directory,
'safe': safe,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));

View file

@ -96,7 +96,8 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
readyCompleter.complete();
}
});
await source.init(canAnalyze: false);
source.canAnalyze = false;
await source.init();
await readyCompleter.future;
final entries = CollectionLens(source: source, filters: filters).sortedEntries;

View file

@ -683,7 +683,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
Future<void> _onAnalysisCompletion() async {
debugPrint('Analysis completed');
if (_mediaStoreSource.initState != SourceInitializationState.none) {
if (_mediaStoreSource.scope != SourceScope.none) {
await _mediaStoreSource.loadCatalogMetadata();
await _mediaStoreSource.loadAddresses();
_mediaStoreSource.updateDerivedFilters();

View file

@ -35,9 +35,10 @@ Future<String?> pickAlbum({
required MoveType? moveType,
}) async {
final source = context.read<CollectionSource>();
if (source.initState != SourceInitializationState.full) {
if (source.scope != SourceScope.full) {
await reportService.log('Complete source initialization to pick album');
// source may not be fully initialized in view mode
source.canAnalyze = true;
await source.init();
}
final filter = await Navigator.maybeOf(context)?.push(

View file

@ -98,7 +98,6 @@ class _HomePageState extends State<HomePage> {
var appMode = AppMode.main;
var error = false;
final intentData = widget.intentData ?? await IntentService.getIntentData();
final safeMode = (intentData[IntentDataKeys.safeMode] as bool?) ?? false;
final intentAction = intentData[IntentDataKeys.action] as String?;
_initialFilters = null;
_initialExplorerPath = null;
@ -223,19 +222,16 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
source.safeMode = safeMode;
if (source.initState != SourceInitializationState.full) {
await reportService.log('Initialize source (init state=${source.initState.name}) to start app with mode=$appMode');
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
);
if (source.scope != SourceScope.full) {
await reportService.log('Initialize source (init state=${source.scope.name}) to start app with mode=$appMode');
final loadTopEntriesFirst = settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty;
await source.init(loadTopEntriesFirst: loadTopEntriesFirst);
}
case AppMode.screenSaver:
final source = context.read<CollectionSource>();
await reportService.log('Initialize source to start screen saver');
await source.init(
canAnalyze: false,
);
final source = context.read<CollectionSource>();
source.canAnalyze = false;
await source.init();
case AppMode.view:
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
final directory = _viewerEntry?.directory;
@ -243,10 +239,8 @@ class _HomePageState extends State<HomePage> {
unawaited(AnalysisService.registerCallback());
await reportService.log('Initialize source to view item in directory $directory');
final source = context.read<CollectionSource>();
await source.init(
directory: directory,
canAnalyze: false,
);
source.canAnalyze = false;
await source.init(albumFilter: AlbumFilter(directory, null));
}
} else {
await _initViewerEssentials();
@ -311,7 +305,7 @@ class _HomePageState extends State<HomePage> {
CollectionLens? collection;
final source = context.read<CollectionSource>();
if (source.initState != SourceInitializationState.none) {
if (source.scope != SourceScope.none) {
final album = viewerEntry.directory;
if (album != null) {
// wait for collection to pass the `loading` state

View file

@ -437,7 +437,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback);
} else {
final source = context.read<CollectionSource>();
if (source.initState != SourceInitializationState.none) {
if (source.scope != SourceScope.none) {
await source.removeEntries({targetEntry.uri}, includeTrash: true);
}
EntryDeletedNotification({targetEntry}).dispatch(context);

View file

@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1;

View file

@ -109,7 +109,7 @@ void main() {
final source = MediaStoreSource();
unawaited(source.init());
await Future.delayed(const Duration(milliseconds: 10));
expect(source.initState, SourceInitializationState.full);
expect(source.scope, SourceScope.full);
await source.refreshUris({refreshEntry.uri});
await Future.delayed(const Duration(seconds: 1));