import 'dart:async'; import 'dart:ui'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/source_state.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; class AnalysisService { static const _platform = MethodChannel('deckers.thibault/aves/analysis'); static Future registerCallback() async { try { await _platform.invokeMethod('registerCallback', { // callback needs to be annotated with `@pragma('vm:entry-point')` to work in release mode 'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(), }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } static Future startService({required bool force, List? entryIds}) async { try { await _platform.invokeMethod('startService', { 'entryIds': entryIds, 'force': force, }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } } const _channel = MethodChannel('deckers.thibault/aves/analysis_service_background'); @pragma('vm:entry-point') Future _init() async { WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); await androidFileUtils.init(); await metadataDb.init(); await mobileServices.init(); await settings.init(monitorPlatformSettings: false); FijkLog.setLevel(FijkLogLevel.Warn); await reportService.init(); final analyzer = Analyzer(); _channel.setMethodCallHandler((call) { switch (call.method) { case 'start': analyzer.start(call.arguments); return Future.value(true); case 'stop': analyzer.stop(); return Future.value(true); default: throw PlatformException(code: 'not-implemented', message: 'failed to handle method=${call.method}'); } }); try { await _channel.invokeMethod('initialized'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } enum AnalyzerState { running, stopping, stopped } class Analyzer { late AppLocalizations _l10n; final ValueNotifier _serviceStateNotifier = ValueNotifier(AnalyzerState.stopped); AnalysisController? _controller; Timer? _notificationUpdateTimer; final _source = MediaStoreSource(); AnalyzerState get serviceState => _serviceStateNotifier.value; bool get isRunning => serviceState == AnalyzerState.running; SourceState get sourceState => _source.state; static const notificationUpdateInterval = Duration(seconds: 1); Analyzer() { debugPrint('$runtimeType create'); _serviceStateNotifier.addListener(_onServiceStateChanged); _source.stateNotifier.addListener(_onSourceStateChanged); } void dispose() { debugPrint('$runtimeType dispose'); _serviceStateNotifier.removeListener(_onServiceStateChanged); _source.stateNotifier.removeListener(_onSourceStateChanged); _stopUpdateTimer(); } Future start(dynamic args) async { List? entryIds; var force = false; if (args is Map) { entryIds = (args['entryIds'] as List?)?.cast(); force = args['force'] ?? false; } debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries'); _controller = AnalysisController( canStartService: false, entryIds: entryIds, force: force, stopSignal: ValueNotifier(false), ); settings.systemLocalesFallback = await deviceService.getLocales(); _l10n = await AppLocalizations.delegate.load(settings.appliedLocale); _serviceStateNotifier.value = AnalyzerState.running; await _source.init(analysisController: _controller); _notificationUpdateTimer = Timer.periodic(notificationUpdateInterval, (_) async { if (!isRunning) return; await _updateNotification(); }); } void stop() { debugPrint('$runtimeType stop'); _serviceStateNotifier.value = AnalyzerState.stopped; } void _stopUpdateTimer() => _notificationUpdateTimer?.cancel(); Future _onServiceStateChanged() async { switch (serviceState) { case AnalyzerState.running: break; case AnalyzerState.stopping: await _stopPlatformService(); _serviceStateNotifier.value = AnalyzerState.stopped; break; case AnalyzerState.stopped: _controller?.stopSignal.value = true; _stopUpdateTimer(); break; } } void _onSourceStateChanged() { if (_source.isReady) { _refreshApp(); _serviceStateNotifier.value = AnalyzerState.stopping; } } Future _updateNotification() async { if (!isRunning) return; final title = sourceState.getName(_l10n); if (title == null) return; final progress = _source.progressNotifier.value; final progressive = progress.total != 0 && sourceState != SourceState.locatingCountries; try { await _channel.invokeMethod('updateNotification', { 'title': title, 'message': progressive ? '${progress.done}/${progress.total}' : null, }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } Future _refreshApp() async { try { await _channel.invokeMethod('refreshApp'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } Future _stopPlatformService() async { try { await _channel.invokeMethod('stop'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } }