loader/completer review

This commit is contained in:
Thibault Deckers 2024-10-29 23:11:35 +01:00
parent f0e048e340
commit b5fea82fce
12 changed files with 79 additions and 86 deletions

View file

@ -461,17 +461,17 @@ class AvesEntry with AvesEntryBase {
} }
Future<bool> delete() { Future<bool> delete() {
final completer = Completer<bool>(); final opCompleter = Completer<bool>();
mediaEditService.delete(entries: {this}).listen( mediaEditService.delete(entries: {this}).listen(
(event) => completer.complete(event.success && !event.skipped), (event) => opCompleter.complete(event.success && !event.skipped),
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) { if (!opCompleter.isCompleted) {
completer.complete(false); opCompleter.complete(false);
} }
}, },
); );
return completer.future; return opCompleter.future;
} }
// when the MIME type or the image itself changed (e.g. after rotation) // when the MIME type or the image itself changed (e.g. after rotation)

View file

@ -23,6 +23,7 @@ class MediaStoreSource extends CollectionSource {
int? _lastGeneration; int? _lastGeneration;
SourceScope _loadedScope, _targetScope; SourceScope _loadedScope, _targetScope;
bool _canAnalyze = true; bool _canAnalyze = true;
Future<void>? _essentialLoader;
@override @override
set canAnalyze(bool enabled) => _canAnalyze = enabled; set canAnalyze(bool enabled) => _canAnalyze = enabled;
@ -41,7 +42,8 @@ class MediaStoreSource extends CollectionSource {
}) async { }) async {
_targetScope = scope; _targetScope = scope;
await reportService.log('$runtimeType init target scope=$scope'); await reportService.log('$runtimeType init target scope=$scope');
await _loadEssentials(); _essentialLoader ??= _loadEssentials();
await _essentialLoader;
addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet()); addDirectories(albums: settings.pinnedFilters.whereType<AlbumFilter>().map((v) => v.album).toSet());
await updateGeneration(); await updateGeneration();
unawaited(_loadEntries( unawaited(_loadEntries(
@ -50,11 +52,7 @@ class MediaStoreSource extends CollectionSource {
)); ));
} }
bool _areEssentialsLoaded = false;
Future<void> _loadEssentials() async { Future<void> _loadEssentials() async {
if (_areEssentialsLoaded) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
state = SourceState.loading; state = SourceState.loading;
await localMediaDb.init(); await localMediaDb.init();
@ -72,7 +70,6 @@ class MediaStoreSource extends CollectionSource {
} }
} }
await loadDates(); await loadDates();
_areEssentialsLoaded = true;
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms'); debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
} }
@ -260,7 +257,7 @@ class MediaStoreSource extends CollectionSource {
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg` // sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override @override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async { Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (!canRefresh || !_areEssentialsLoaded || !isReady) return changedUris; if (!canRefresh || _essentialLoader == null || !isReady) return changedUris;
state = SourceState.loading; state = SourceState.loading;

View file

@ -24,18 +24,18 @@ mixin TrashMixin on SourceBase {
if (expiredEntries.isEmpty) return {}; if (expiredEntries.isEmpty) return {};
final processed = <ImageOpEvent>{}; final processed = <ImageOpEvent>{};
final completer = Completer<Set<String>>(); final opCompleter = Completer<Set<String>>();
mediaEditService.delete(entries: expiredEntries).listen( mediaEditService.delete(entries: expiredEntries).listen(
processed.add, processed.add,
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () async { onDone: () async {
final successOps = processed.where((e) => e.success).toSet(); final successOps = processed.where((e) => e.success).toSet();
final deletedOps = successOps.where((e) => !e.skipped).toSet(); final deletedOps = successOps.where((e) => !e.skipped).toSet();
final deletedUris = deletedOps.map((event) => event.uri).toSet(); final deletedUris = deletedOps.map((event) => event.uri).toSet();
completer.complete(deletedUris); opCompleter.complete(deletedUris);
}, },
); );
return await completer.future; return await opCompleter.future;
} }
Future<Set<AvesEntry>> recoverUntrackedTrashItems() async { Future<Set<AvesEntry>> recoverUntrackedTrashItems() async {

View file

@ -104,21 +104,21 @@ class PlatformAppService implements AppService {
@override @override
Future<Map<String, dynamic>> edit(String uri, String mimeType) async { Future<Map<String, dynamic>> edit(String uri, String mimeType) async {
try { try {
final completer = Completer<Map?>(); final opCompleter = Completer<Map?>();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'edit', 'op': 'edit',
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}).listen( }).listen(
(data) => completer.complete(data as Map?), (data) => opCompleter.complete(data as Map?),
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) completer.complete({'error': 'cancelled'}); if (!opCompleter.isCompleted) opCompleter.complete({'error': 'cancelled'});
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
final result = await completer.future; final result = await opCompleter.future;
if (result == null) return {'error': 'cancelled'}; if (result == null) return {'error': 'cancelled'};
return result.cast<String, dynamic>(); return result.cast<String, dynamic>();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {

View file

@ -24,32 +24,32 @@ class ServicePolicy {
int priority = ServiceCallPriority.normal, int priority = ServiceCallPriority.normal,
Object? key, Object? key,
}) { }) {
Completer<T> completer; Completer<T> taskCompleter;
_Task<T> task; _Task<T> task;
key ??= platformCall.hashCode; key ??= platformCall.hashCode;
final toResume = _paused.remove(key); final toResume = _paused.remove(key);
if (toResume != null) { if (toResume != null) {
priority = toResume.$1; priority = toResume.$1;
task = toResume.$2 as _Task<T>; task = toResume.$2 as _Task<T>;
completer = task.completer; taskCompleter = task.completer;
} else { } else {
completer = Completer<T>(); taskCompleter = Completer<T>();
task = _Task<T>( task = _Task<T>(
() async { () async {
try { try {
completer.complete(await platformCall()); taskCompleter.complete(await platformCall());
} catch (error, stack) { } catch (error, stack) {
completer.completeError(error, stack); taskCompleter.completeError(error, stack);
} }
_runningQueue.remove(key); _runningQueue.remove(key);
_pickNext(); _pickNext();
}, },
completer, taskCompleter,
); );
} }
_getQueue(priority)[key] = task; _getQueue(priority)[key] = task;
_pickNext(); _pickNext();
return completer.future; return taskCompleter.future;
} }
Future<T>? resume<T>(Object key) { Future<T>? resume<T>(Object key) {

View file

@ -47,23 +47,23 @@ class IntentService {
static Future<Set<CollectionFilter>?> pickCollectionFilters(Set<CollectionFilter>? initialFilters) async { static Future<Set<CollectionFilter>?> pickCollectionFilters(Set<CollectionFilter>? initialFilters) async {
try { try {
final completer = Completer<Set<CollectionFilter>?>(); final opCompleter = Completer<Set<CollectionFilter>?>();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'pickCollectionFilters', 'op': 'pickCollectionFilters',
'initialFilters': initialFilters?.map((filter) => filter.toJson()).toList(), 'initialFilters': initialFilters?.map((filter) => filter.toJson()).toList(),
}).listen( }).listen(
(data) { (data) {
final result = (data as List?)?.cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(); final result = (data as List?)?.cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
completer.complete(result); opCompleter.complete(result);
}, },
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) completer.complete(null); if (!opCompleter.isCompleted) opCompleter.complete(null);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }

View file

@ -120,7 +120,7 @@ class PlatformMediaFetchService implements MediaFetchService {
BytesReceivedCallback? onBytesReceived, BytesReceivedCallback? onBytesReceived,
}) async { }) async {
try { try {
final completer = Completer<Uint8List>.sync(); final opCompleter = Completer<Uint8List>();
final sink = OutputBuffer(); final sink = OutputBuffer();
var bytesReceived = 0; var bytesReceived = 0;
_byteStream.receiveBroadcastStream(<String, dynamic>{ _byteStream.receiveBroadcastStream(<String, dynamic>{
@ -139,20 +139,20 @@ class PlatformMediaFetchService implements MediaFetchService {
try { try {
onBytesReceived(bytesReceived, sizeBytes); onBytesReceived(bytesReceived, sizeBytes);
} catch (error, stack) { } catch (error, stack) {
completer.completeError(error, stack); opCompleter.completeError(error, stack);
return; return;
} }
} }
}, },
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
sink.close(); sink.close();
completer.complete(sink.bytes); opCompleter.complete(sink.bytes);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (_isUnknownVisual(mimeType)) { if (_isUnknownVisual(mimeType)) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);

View file

@ -265,20 +265,20 @@ class PlatformStorageService implements StorageService {
@override @override
Future<bool> requestDirectoryAccess(String path) async { Future<bool> requestDirectoryAccess(String path) async {
try { try {
final completer = Completer<bool>(); final opCompleter = Completer<bool>();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'requestDirectoryAccess', 'op': 'requestDirectoryAccess',
'path': path, 'path': path,
}).listen( }).listen(
(data) => completer.complete(data as bool), (data) => opCompleter.complete(data as bool),
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) completer.complete(false); if (!opCompleter.isCompleted) opCompleter.complete(false);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -289,21 +289,21 @@ class PlatformStorageService implements StorageService {
@override @override
Future<bool> requestMediaFileAccess(List<String> uris, List<String> mimeTypes) async { Future<bool> requestMediaFileAccess(List<String> uris, List<String> mimeTypes) async {
try { try {
final completer = Completer<bool>(); final opCompleter = Completer<bool>();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'requestMediaFileAccess', 'op': 'requestMediaFileAccess',
'uris': uris, 'uris': uris,
'mimeTypes': mimeTypes, 'mimeTypes': mimeTypes,
}).listen( }).listen(
(data) => completer.complete(data as bool), (data) => opCompleter.complete(data as bool),
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) completer.complete(false); if (!opCompleter.isCompleted) opCompleter.complete(false);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
final message = e.message; final message = e.message;
// mute issue in the specific case when an item: // mute issue in the specific case when an item:
@ -320,22 +320,22 @@ class PlatformStorageService implements StorageService {
@override @override
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async { Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
try { try {
final completer = Completer<bool?>(); final opCompleter = Completer<bool?>();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'createFile', 'op': 'createFile',
'name': name, 'name': name,
'mimeType': mimeType, 'mimeType': mimeType,
'bytes': bytes, 'bytes': bytes,
}).listen( }).listen(
(data) => completer.complete(data as bool?), (data) => opCompleter.complete(data as bool?),
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) completer.complete(false); if (!opCompleter.isCompleted) opCompleter.complete(false);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -345,7 +345,7 @@ class PlatformStorageService implements StorageService {
@override @override
Future<Uint8List> openFile([String? mimeType]) async { Future<Uint8List> openFile([String? mimeType]) async {
try { try {
final completer = Completer<Uint8List>.sync(); final opCompleter = Completer<Uint8List>();
final sink = OutputBuffer(); final sink = OutputBuffer();
_stream.receiveBroadcastStream(<String, dynamic>{ _stream.receiveBroadcastStream(<String, dynamic>{
'op': 'openFile', 'op': 'openFile',
@ -355,15 +355,15 @@ class PlatformStorageService implements StorageService {
final chunk = data as Uint8List; final chunk = data as Uint8List;
sink.add(chunk); sink.add(chunk);
}, },
onError: completer.completeError, onError: opCompleter.completeError,
onDone: () { onDone: () {
sink.close(); sink.close();
completer.complete(sink.bytes); opCompleter.complete(sink.bytes);
}, },
cancelOnError: true, cancelOnError: true,
); );
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await opCompleter.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/model/app_inventory.dart'; import 'package:aves/model/app_inventory.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';
@ -7,8 +9,6 @@ import 'package:flutter/foundation.dart';
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
enum _State { uninitialized, initializing, initialized }
class AndroidFileUtils { class AndroidFileUtils {
// cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT // cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT
static const contentScheme = 'content'; static const contentScheme = 'content';
@ -29,16 +29,13 @@ class AndroidFileUtils {
late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
late final Set<String> videoCapturesPaths; late final Set<String> videoCapturesPaths;
Set<StorageVolume> storageVolumes = {}; Set<StorageVolume> storageVolumes = {};
_State _initialized = _State.uninitialized; Future<void>? _loader;
AndroidFileUtils._private(); AndroidFileUtils._private();
Future<void> init() async { Future<void> init() async {
if (_initialized == _State.uninitialized) { _loader ??= _doInit();
_initialized = _State.initializing; await _loader;
await _doInit();
_initialized = _State.initialized;
}
} }
Future<void> _doInit() async { Future<void> _doInit() async {

View file

@ -138,7 +138,7 @@ mixin FeedbackMixin {
VoidCallback? onCancel, VoidCallback? onCancel,
Future<void> Function(Set<T> processed)? onDone, Future<void> Function(Set<T> processed)? onDone,
}) async { }) async {
final completer = Completer(); final opCompleter = Completer();
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -149,12 +149,12 @@ mixin FeedbackMixin {
onDone: (processed) async { onDone: (processed) async {
Navigator.maybeOf(context)?.pop(); Navigator.maybeOf(context)?.pop();
await onDone?.call(processed); await onDone?.call(processed);
completer.complete(); opCompleter.complete();
}, },
), ),
routeSettings: const RouteSettings(name: ReportOverlay.routeName), routeSettings: const RouteSettings(name: ReportOverlay.routeName),
); );
return completer.future; return opCompleter.future;
} }
} }

View file

@ -309,18 +309,18 @@ class _HomePageState extends State<HomePage> {
final album = viewerEntry.directory; final album = viewerEntry.directory;
if (album != null) { if (album != null) {
// wait for collection to pass the `loading` state // wait for collection to pass the `loading` state
final completer = Completer(); final loadingCompleter = Completer();
final stateNotifier = source.stateNotifier; final stateNotifier = source.stateNotifier;
void _onSourceStateChanged() { void _onSourceStateChanged() {
if (stateNotifier.value != SourceState.loading) { if (stateNotifier.value != SourceState.loading) {
stateNotifier.removeListener(_onSourceStateChanged); stateNotifier.removeListener(_onSourceStateChanged);
completer.complete(); loadingCompleter.complete();
} }
} }
stateNotifier.addListener(_onSourceStateChanged); stateNotifier.addListener(_onSourceStateChanged);
_onSourceStateChanged(); _onSourceStateChanged();
await completer.future; await loadingCompleter.future;
collection = CollectionLens( collection = CollectionLens(
source: source, source: source,

View file

@ -13,27 +13,26 @@ typedef HistogramLevels = Map<HistogramChannel, List<double>>;
mixin HistogramMixin { mixin HistogramMixin {
HistogramLevels _levels = {}; HistogramLevels _levels = {};
Completer? _completer; Future<void>? _loader;
static const int bins = 256; static const int bins = 256;
Future<HistogramLevels> getHistogramLevels(ImageInfo info, bool forceUpdate) async { Future<HistogramLevels> getHistogramLevels(ImageInfo info, bool forceUpdate) async {
if (_levels.isEmpty || forceUpdate) { if (_levels.isEmpty || forceUpdate) {
if (_completer == null) { _loader ??= _getLevels(info);
_completer = Completer(); await _loader;
_loader = null;
}
return _levels;
}
Future<void> _getLevels(ImageInfo info) async {
final data = (await info.image.toByteData(format: ImageByteFormat.rawStraightRgba))!; final data = (await info.image.toByteData(format: ImageByteFormat.rawStraightRgba))!;
_levels = switch (settings.overlayHistogramStyle) { _levels = switch (settings.overlayHistogramStyle) {
OverlayHistogramStyle.rgb => await compute<ByteData, HistogramLevels>(_computeRgbLevels, data), OverlayHistogramStyle.rgb => await compute<ByteData, HistogramLevels>(_computeRgbLevels, data),
OverlayHistogramStyle.luminance => await compute<ByteData, HistogramLevels>(_computeLuminanceLevels, data), OverlayHistogramStyle.luminance => await compute<ByteData, HistogramLevels>(_computeLuminanceLevels, data),
_ => <HistogramChannel, List<double>>{}, _ => <HistogramChannel, List<double>>{},
}; };
_completer?.complete();
} else {
await _completer?.future;
_completer = null;
}
}
return _levels;
} }
static HistogramLevels _computeRgbLevels(ByteData data) { static HistogramLevels _computeRgbLevels(ByteData data) {