import 'dart:async'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_init.dart' as app_init; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/future_extension.dart'; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/reverse_geocoder.dart'; import 'package:nc_photos/use_case/update_missing_metadata.dart'; import 'package:nc_photos_plugin/nc_photos_plugin.dart'; import 'package:np_codegen/np_codegen.dart'; part 'service.g.dart'; /// Start the background service Future startService() async { _C.log.info("[startService] Starting service"); final service = FlutterBackgroundService(); await service.configure( androidConfiguration: AndroidConfiguration( onStart: serviceMain, autoStart: false, isForegroundMode: true, foregroundServiceNotificationTitle: L10n.global().metadataTaskProcessingNotification, ), iosConfiguration: IosConfiguration( onForeground: () => throw UnimplementedError(), onBackground: () => throw UnimplementedError(), ), ); // sync settings await ServiceConfig.setProcessExifWifiOnly( Pref().shouldProcessExifWifiOnlyOr()); await service.start(); } /// Ask the background service to stop ASAP void stopService() { _C.log.info("[stopService] Stopping service"); FlutterBackgroundService().sendData({ _dataKeyEvent: _eventStop, }); } @visibleForTesting @pragma("vm:entry-point") Future serviceMain() async { _Service._shouldRun.value = true; WidgetsFlutterBinding.ensureInitialized(); await _Service()(); } class ServiceConfig { static Future setProcessExifWifiOnly(bool flag) async { await Preference.setBool(_servicePref, _servicePrefProcessWifiOnly, flag); } } @npLog class _Service { Future call() async { final service = FlutterBackgroundService(); service.setForegroundMode(true); await app_init.init(app_init.InitIsolateType.flutterIsolate); await _L10n().init(); _log.info("[call] Service started"); final onCancelSubscription = service.onCancel.listen((_) { _log.info("[call] User canceled"); _stopSelf(); }); final onDataSubscription = service.onDataReceived.listen((event) => _onReceiveData(event ?? {})); try { await _doWork(); } catch (e, stackTrace) { _log.shout("[call] Uncaught exception", e, stackTrace); } await onCancelSubscription.cancel(); await onDataSubscription.cancel(); await KiwiContainer().resolve().sqliteDb.close(); service.stopBackgroundService(); _log.info("[call] Service stopped"); } Future _doWork() async { final account = Pref().getCurrentAccount(); if (account == null) { _log.shout("[_doWork] account == null"); return; } final accountPref = AccountPref.of(account); final service = FlutterBackgroundService(); final metadataTask = _MetadataTask(service, account, accountPref); _metadataTaskStateChangedListener.begin(); try { await metadataTask(); } finally { _metadataTaskStateChangedListener.end(); } } void _onReceiveData(Map data) { try { final event = data[_dataKeyEvent]; switch (event) { case _eventStop: _stopSelf(); break; default: _log.severe("[_onReceiveData] Unknown event: $event"); break; } } catch (e, stackTrace) { _log.shout("[_onReceiveData] Uncaught exception", e, stackTrace); } } void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) { if (ev.state == _metadataTaskState) { return; } _metadataTaskState = ev.state; if (_isPaused != true) { if (ev.state == MetadataTaskState.waitingForWifi) { FlutterBackgroundService() ..setNotificationInfo( title: _L10n.global().metadataTaskPauseNoWiFiNotification, ) ..pauseWakeLock(); _isPaused = true; } else if (ev.state == MetadataTaskState.lowBattery) { FlutterBackgroundService() ..setNotificationInfo( title: _L10n.global().metadataTaskPauseLowBatteryNotification, ) ..pauseWakeLock(); _isPaused = true; } } else { if (ev.state == MetadataTaskState.prcoessing) { FlutterBackgroundService().resumeWakeLock(); _isPaused = false; } } } void _stopSelf() { _log.info("[_stopSelf] Stopping service"); FlutterBackgroundService().setNotificationInfo( title: _L10n.global().backgroundServiceStopping, ); _shouldRun.value = false; } var _metadataTaskState = MetadataTaskState.idle; late final _metadataTaskStateChangedListener = AppEventListener( _onMetadataTaskStateChanged); bool? _isPaused; static final _shouldRun = ValueNotifier(true); } /// Access localized string out of the main isolate @npLog class _L10n { factory _L10n() => _inst; _L10n._(); Future init() async { try { final locale = language_util.getSelectedLocale(); if (locale == null) { _l10n = await _queryL10n(); } else { _l10n = lookupAppLocalizations(locale); } } catch (e, stackTrace) { _log.shout("[init] Uncaught exception", e, stackTrace); _l10n = AppLocalizationsEn(); } } static AppLocalizations global() => _L10n()._l10n; Future _queryL10n() async { try { final locale = await Devicelocale.currentAsLocale; return lookupAppLocalizations(locale!); } on FlutterError catch (_) { // unsupported locale, use default (en) return AppLocalizationsEn(); } catch (e, stackTrace) { _log.shout( "[_queryL10n] Failed while lookupAppLocalizations", e, stackTrace); return AppLocalizationsEn(); } } static final _inst = _L10n._(); late AppLocalizations _l10n; } @npLog class _MetadataTask { _MetadataTask(this.service, this.account, this.accountPref); Future call() async { try { await _updateMetadata(); } catch (e, stackTrace) { _log.shout("[call] Uncaught exception", e, stackTrace); } if (_processedIds.isNotEmpty) { unawaited( NativeEvent.fire(FileExifUpdatedEvent(_processedIds).toEvent()), ); _processedIds = []; } final c = KiwiContainer().resolve(); if (c.fileRepo.dataSrc is FileCachedDataSource) { await (c.fileRepo.dataSrc as FileCachedDataSource).flushRemoteTouch(); } } Future _updateMetadata() async { final shareFolder = File( path: file_util.unstripPath(account, accountPref.getShareFolderOr())); bool hasScanShareFolder = false; final c = KiwiContainer().resolve(); final geocoder = ReverseGeocoder(); await geocoder.init(); for (final r in account.roots) { final dir = File(path: file_util.unstripPath(account, r)); hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir); final updater = UpdateMissingMetadata( c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder); void onServiceStop() { _log.info("[_updateMetadata] Stopping task: user canceled"); updater.stop(); _shouldRun = false; } _Service._shouldRun.addListener(onServiceStop); try { await for (final ev in updater(account, dir)) { if (ev is File) { _onFileProcessed(ev); } } } finally { _Service._shouldRun.removeListener(onServiceStop); } if (!_shouldRun) { return; } } if (!hasScanShareFolder) { final shareUpdater = UpdateMissingMetadata( c.fileRepo, const _UpdateMissingMetadataConfigProvider(), geocoder); void onServiceStop() { _log.info("[_updateMetadata] Stopping task: user canceled"); shareUpdater.stop(); _shouldRun = false; } _Service._shouldRun.addListener(onServiceStop); try { await for (final ev in shareUpdater( account, shareFolder, isRecursive: false, filter: (f) => f.ownerId != account.userId, )) { if (ev is File) { _onFileProcessed(ev); } } } finally { _Service._shouldRun.removeListener(onServiceStop); } if (!_shouldRun) { return; } } } void _onFileProcessed(File file) { ++_count; service.setNotificationInfo( title: _L10n.global().metadataTaskProcessingNotification, content: file.strippedPath, ); _processedIds.add(file.fileId!); if (_processedIds.length >= 10) { NativeEvent.fire(FileExifUpdatedEvent(_processedIds).toEvent()); _processedIds = []; } } final FlutterBackgroundService service; final Account account; final AccountPref accountPref; var _shouldRun = true; var _count = 0; var _processedIds = []; } class _UpdateMissingMetadataConfigProvider implements UpdateMissingMetadataConfigProvider { const _UpdateMissingMetadataConfigProvider(); @override isWifiOnly() => Preference.getBool(_servicePref, _servicePrefProcessWifiOnly, true) .notNull(); } class _C { // needed to work with generator logger static final log = Logger("service"); } const _dataKeyEvent = "event"; const _eventStop = "stop"; const _servicePref = "service"; const _servicePrefProcessWifiOnly = "shouldProcessWifiOnly";