rescan with service

This commit is contained in:
Thibault Deckers 2021-10-18 11:46:42 +09:00
parent 85a0c504d6
commit 4d4da21090
10 changed files with 89 additions and 44 deletions

View file

@ -157,12 +157,17 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
val context = this@AnalysisService val context = this@AnalysisService
val data = msg.data val data = msg.data
Log.d(LOG_TAG, "handleMessage data=$data")
when (data.getString(KEY_COMMAND)) { when (data.getString(KEY_COMMAND)) {
COMMAND_START -> { COMMAND_START -> {
runBlocking { runBlocking {
context.runOnUiThread { context.runOnUiThread {
backgroundChannel?.invokeMethod("start", null) val contentIds = data.get(KEY_CONTENT_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() }
backgroundChannel?.invokeMethod(
"start", hashMapOf(
"contentIds" to contentIds,
"force" to data.getBoolean(KEY_FORCE),
)
)
} }
} }
} }
@ -194,6 +199,8 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
const val KEY_COMMAND = "command" const val KEY_COMMAND = "command"
const val COMMAND_START = "start" const val COMMAND_START = "start"
const val COMMAND_STOP = "stop" const val COMMAND_STOP = "stop"
const val KEY_CONTENT_IDS = "content_ids"
const val KEY_FORCE = "force"
} }
} }

View file

@ -44,10 +44,21 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
result.success(true) result.success(true)
} }
private fun startAnalysis(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) {
val force = call.argument<Boolean>("force")
if (force == null) {
result.error("startAnalysis-args", "failed because of missing arguments", null)
return
}
// can be null or empty
val contentIds = call.argument<List<Int>>("contentIds");
if (!activity.isMyServiceRunning(AnalysisService::class.java)) { if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
val intent = Intent(activity, AnalysisService::class.java) val intent = Intent(activity, AnalysisService::class.java)
intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START) intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
intent.putExtra(AnalysisService.KEY_CONTENT_IDS, contentIds?.toIntArray())
intent.putExtra(AnalysisService.KEY_FORCE, force)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent) activity.startForegroundService(intent)
} else { } else {

View file

@ -2,10 +2,12 @@ import 'package:flutter/foundation.dart';
class AnalysisController { class AnalysisController {
final bool canStartService, force; final bool canStartService, force;
final List<int>? contentIds;
final ValueNotifier<bool> stopSignal; final ValueNotifier<bool> stopSignal;
AnalysisController({ AnalysisController({
this.canStartService = true, this.canStartService = true,
this.contentIds,
this.force = false, this.force = false,
ValueNotifier<bool>? stopSignal, ValueNotifier<bool>? stopSignal,
}) : stopSignal = stopSignal ?? ValueNotifier(false); }) : stopSignal = stopSignal ?? ValueNotifier(false);

View file

@ -37,8 +37,6 @@ mixin SourceBase {
} }
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
static const _analysisServiceOpCountThreshold = 400;
final EventBus _eventBus = EventBus(); final EventBus _eventBus = EventBus();
@override @override
@ -269,30 +267,39 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
eventBus.fire(EntryRefreshedEvent({entry})); eventBus.fire(EntryRefreshedEvent({entry}));
} }
Future<void> analyze(AnalysisController? analysisController, Set<AvesEntry> candidateEntries) async { Future<void> analyze(AnalysisController? analysisController, {Set<AvesEntry>? entries}) async {
final todoEntries = visibleEntries; final todoEntries = entries ?? visibleEntries;
final _analysisController = analysisController ?? AnalysisController(); final _analysisController = analysisController ?? AnalysisController();
final force = _analysisController.force;
if (!_analysisController.isStopping) { if (!_analysisController.isStopping) {
late bool startAnalysisService; var startAnalysisService = false;
if (_analysisController.canStartService && settings.canUseAnalysisService) { if (_analysisController.canStartService && settings.canUseAnalysisService) {
final force = _analysisController.force; // cataloguing
var opCount = 0; if (!startAnalysisService) {
opCount += (force ? todoEntries : todoEntries.where(TagMixin.catalogEntriesTest)).length; final opCount = (force ? todoEntries : todoEntries.where(TagMixin.catalogEntriesTest)).length;
opCount += (force ? todoEntries.where((entry) => entry.hasGps) : todoEntries.where(LocationMixin.locateCountriesTest)).length; if (opCount > TagMixin.commitCountThreshold) {
if (await availability.canLocatePlaces) { startAnalysisService = true;
opCount += (force ? todoEntries.where((entry) => entry.hasGps) : todoEntries.where(LocationMixin.locatePlacesTest)).length; }
}
// ignore locating countries
// locating places
if (!startAnalysisService && await availability.canLocatePlaces) {
final opCount = (force ? todoEntries.where((entry) => entry.hasGps) : todoEntries.where(LocationMixin.locatePlacesTest)).length;
if (opCount > LocationMixin.commitCountThreshold) {
startAnalysisService = true;
}
} }
startAnalysisService = opCount > _analysisServiceOpCountThreshold;
} else {
startAnalysisService = false;
} }
if (startAnalysisService) { if (startAnalysisService) {
await AnalysisService.startService(); await AnalysisService.startService(
force: force,
contentIds: entries?.map((entry) => entry.contentId).whereNotNull().toList(),
);
} else { } else {
await catalogEntries(_analysisController, candidateEntries); await catalogEntries(_analysisController, todoEntries);
updateDerivedFilters(candidateEntries); updateDerivedFilters(todoEntries);
await locateEntries(_analysisController, candidateEntries); await locateEntries(_analysisController, todoEntries);
updateDerivedFilters(candidateEntries); updateDerivedFilters(todoEntries);
} }
} }
stateNotifier.value = SourceState.ready; stateNotifier.value = SourceState.ready;
@ -347,7 +354,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
if (visible) { if (visible) {
final candidateEntries = visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet(); final candidateEntries = visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet();
analyze(null, candidateEntries); analyze(null, entries: candidateEntries);
} }
} }
} }

View file

@ -14,8 +14,8 @@ import 'package:flutter/foundation.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
mixin LocationMixin on SourceBase { mixin LocationMixin on SourceBase {
static const _commitCountThreshold = 80; static const commitCountThreshold = 200;
static const _stopCheckCountThreshold = 20; static const _stopCheckCountThreshold = 50;
List<String> sortedCountries = List.unmodifiable([]); List<String> sortedCountries = List.unmodifiable([]);
List<String> sortedPlaces = List.unmodifiable([]); List<String> sortedPlaces = List.unmodifiable([]);
@ -118,7 +118,7 @@ mixin LocationMixin on SourceBase {
} }
if (entry.hasFineAddress) { if (entry.hasFineAddress) {
newAddresses.add(entry.addressDetails!); newAddresses.add(entry.addressDetails!);
if (newAddresses.length >= _commitCountThreshold) { if (newAddresses.length >= commitCountThreshold) {
await metadataDb.saveAddresses(Set.unmodifiable(newAddresses)); await metadataDb.saveAddresses(Set.unmodifiable(newAddresses));
onAddressMetadataChanged(); onAddressMetadataChanged();
newAddresses.clear(); newAddresses.clear();

View file

@ -111,7 +111,12 @@ class MediaStoreSource extends CollectionSource {
updateDirectories(); updateDirectories();
} }
await analyze(analysisController, visibleEntries); Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.contentIds;
if (analysisIds != null) {
analysisEntries = visibleEntries.where((entry) => analysisIds.contains(entry.contentId)).toSet();
}
await analyze(analysisController, entries: analysisEntries);
debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${oldEntries.length} known, ${allNewEntries.length} new, ${obsoleteContentIds.length} obsolete'); debugPrint('$runtimeType refresh ${stopwatch.elapsed} done for ${oldEntries.length} known, ${allNewEntries.length} new, ${obsoleteContentIds.length} obsolete');
}, },
@ -179,7 +184,7 @@ class MediaStoreSource extends CollectionSource {
await metadataDb.saveEntries(newEntries); await metadataDb.saveEntries(newEntries);
cleanEmptyAlbums(existingDirectories); cleanEmptyAlbums(existingDirectories);
await analyze(analysisController, newEntries); await analyze(analysisController, entries: newEntries);
} }
return tempUris; return tempUris;

View file

@ -9,7 +9,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
mixin TagMixin on SourceBase { mixin TagMixin on SourceBase {
static const _commitCountThreshold = 400; static const commitCountThreshold = 400;
static const _stopCheckCountThreshold = 100; static const _stopCheckCountThreshold = 100;
List<String> sortedTags = List.unmodifiable([]); List<String> sortedTags = List.unmodifiable([]);
@ -41,7 +41,7 @@ mixin TagMixin on SourceBase {
await entry.catalog(background: true, persist: true, force: force); await entry.catalog(background: true, persist: true, force: force);
if (entry.isCatalogued) { if (entry.isCatalogued) {
newMetadata.add(entry.catalogMetadata!); newMetadata.add(entry.catalogMetadata!);
if (newMetadata.length >= _commitCountThreshold) { if (newMetadata.length >= commitCountThreshold) {
await metadataDb.saveMetadata(Set.unmodifiable(newMetadata)); await metadataDb.saveMetadata(Set.unmodifiable(newMetadata));
onCatalogMetadataChanged(); onCatalogMetadataChanged();
newMetadata.clear(); newMetadata.clear();

View file

@ -25,9 +25,12 @@ class AnalysisService {
} }
} }
static Future<void> startService() async { static Future<void> startService({required bool force, List<int>? contentIds}) async {
try { try {
await platform.invokeMethod('startService'); await platform.invokeMethod('startService', <String, dynamic>{
'contentIds': contentIds,
'force': force,
});
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -48,7 +51,7 @@ Future<void> _init() async {
_channel.setMethodCallHandler((call) { _channel.setMethodCallHandler((call) {
switch (call.method) { switch (call.method) {
case 'start': case 'start':
analyzer.start(); analyzer.start(call.arguments);
return Future.value(true); return Future.value(true);
case 'stop': case 'stop':
analyzer.stop(); analyzer.stop();
@ -69,7 +72,7 @@ enum AnalyzerState { running, stopping, stopped }
class Analyzer { class Analyzer {
late AppLocalizations _l10n; late AppLocalizations _l10n;
final ValueNotifier<AnalyzerState> _serviceStateNotifier = ValueNotifier<AnalyzerState>(AnalyzerState.stopped); final ValueNotifier<AnalyzerState> _serviceStateNotifier = ValueNotifier<AnalyzerState>(AnalyzerState.stopped);
final AnalysisController _controller = AnalysisController(canStartService: false, stopSignal: ValueNotifier(false)); AnalysisController? _controller;
Timer? _notificationUpdateTimer; Timer? _notificationUpdateTimer;
final _source = MediaStoreSource(); final _source = MediaStoreSource();
@ -94,13 +97,23 @@ class Analyzer {
_stopUpdateTimer(); _stopUpdateTimer();
} }
Future<void> start() async { Future<void> start(dynamic args) async {
debugPrint('$runtimeType start'); debugPrint('$runtimeType start');
_serviceStateNotifier.value = AnalyzerState.running; List<int>? contentIds;
var force = false;
if (args is Map) {
contentIds = (args['contentIds'] as List?)?.cast<int>();
force = args['force'] ?? false;
}
_controller = AnalysisController(
canStartService: false,
contentIds: contentIds,
force: force,
stopSignal: ValueNotifier(false),
);
_l10n = await AppLocalizations.delegate.load(settings.appliedLocale); _l10n = await AppLocalizations.delegate.load(settings.appliedLocale);
_serviceStateNotifier.value = AnalyzerState.running;
_controller.stopSignal.value = false;
await _source.init(); await _source.init();
unawaited(_source.refresh(analysisController: _controller)); unawaited(_source.refresh(analysisController: _controller));
@ -126,7 +139,7 @@ class Analyzer {
_serviceStateNotifier.value = AnalyzerState.stopped; _serviceStateNotifier.value = AnalyzerState.stopped;
break; break;
case AnalyzerState.stopped: case AnalyzerState.stopped:
_controller.stopSignal.value = true; _controller?.stopSignal.value = true;
_stopUpdateTimer(); _stopUpdateTimer();
break; break;
} }

View file

@ -76,8 +76,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection); final selectedItems = _getExpandedSelectedItems(selection);
final controller = AnalysisController(canStartService: false, force: true); final controller = AnalysisController(canStartService: true, force: true);
source.analyze(controller, selectedItems); source.analyze(controller, entries: selectedItems);
selection.browse(); selection.browse();
} }

View file

@ -104,9 +104,9 @@ class _AppDebugPageState extends State<AppDebugPage> {
}, },
child: const Text('Source full refresh'), child: const Text('Source full refresh'),
), ),
const ElevatedButton( ElevatedButton(
onPressed: AnalysisService.startService, onPressed: () => AnalysisService.startService(force: false),
child: Text('Start analysis service'), child: const Text('Start analysis service'),
), ),
const Divider(), const Divider(),
Padding( Padding(