2022-04-01 18:59:18 +02:00
|
|
|
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:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/account.dart';
|
|
|
|
import 'package:nc_photos/app_db.dart';
|
|
|
|
import 'package:nc_photos/app_init.dart' as app_init;
|
|
|
|
import 'package:nc_photos/app_localizations.dart';
|
|
|
|
import 'package:nc_photos/entity/file.dart';
|
|
|
|
import 'package:nc_photos/entity/file/data_source.dart';
|
|
|
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
|
|
|
import 'package:nc_photos/event/event.dart';
|
|
|
|
import 'package:nc_photos/event/native_event.dart';
|
|
|
|
import 'package:nc_photos/language_util.dart' as language_util;
|
|
|
|
import 'package:nc_photos/pref.dart';
|
|
|
|
import 'package:nc_photos/use_case/update_missing_metadata.dart';
|
|
|
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
|
|
|
|
|
|
|
/// Start the background service
|
|
|
|
Future<void> startService() async {
|
|
|
|
_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(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
await service.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ask the background service to stop ASAP
|
|
|
|
void stopService() {
|
|
|
|
_log.info("[stopService] Stopping service");
|
|
|
|
FlutterBackgroundService().sendData({
|
|
|
|
"stop": true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@visibleForTesting
|
|
|
|
void serviceMain() async {
|
2022-04-08 22:16:35 +02:00
|
|
|
_Service._shouldRun.value = true;
|
2022-04-01 18:59:18 +02:00
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
|
|
|
|
await _Service()();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Service {
|
|
|
|
Future<void> call() async {
|
|
|
|
await app_init.initAppLaunch();
|
|
|
|
await _L10n().init();
|
|
|
|
|
|
|
|
_log.info("[call] Service started");
|
|
|
|
final service = FlutterBackgroundService();
|
|
|
|
service.setForegroundMode(true);
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
onCancelSubscription.cancel();
|
|
|
|
onDataSubscription.cancel();
|
|
|
|
service.stopBackgroundService();
|
|
|
|
_log.info("[call] Service stopped");
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _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<String, dynamic> data) {
|
|
|
|
try {
|
|
|
|
for (final e in data.entries) {
|
|
|
|
switch (e.key) {
|
|
|
|
case "stop":
|
|
|
|
_stopSelf();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout("[_onReceiveData] Uncaught exception", e, stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
|
|
|
|
if (ev.state == _metadataTaskState) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_metadataTaskState = ev.state;
|
2022-04-08 21:38:21 +02:00
|
|
|
if (_isPaused != true) {
|
|
|
|
if (ev.state == MetadataTaskState.waitingForWifi) {
|
|
|
|
FlutterBackgroundService()
|
|
|
|
..setNotificationInfo(
|
|
|
|
title: _L10n.global().metadataTaskPauseNoWiFiNotification,
|
|
|
|
)
|
|
|
|
..pauseWakeLock();
|
|
|
|
_isPaused = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (ev.state == MetadataTaskState.prcoessing) {
|
|
|
|
FlutterBackgroundService().resumeWakeLock();
|
|
|
|
_isPaused = false;
|
|
|
|
}
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _stopSelf() {
|
|
|
|
_log.info("[_stopSelf] Stopping service");
|
|
|
|
FlutterBackgroundService().setNotificationInfo(
|
|
|
|
title: _L10n.global().backgroundServiceStopping,
|
|
|
|
);
|
2022-04-08 22:16:35 +02:00
|
|
|
_shouldRun.value = false;
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var _metadataTaskState = MetadataTaskState.idle;
|
|
|
|
late final _metadataTaskStateChangedListener =
|
|
|
|
AppEventListener<MetadataTaskStateChangedEvent>(
|
|
|
|
_onMetadataTaskStateChanged);
|
|
|
|
|
2022-04-08 21:38:21 +02:00
|
|
|
bool? _isPaused;
|
|
|
|
|
2022-04-08 22:16:35 +02:00
|
|
|
static final _shouldRun = ValueNotifier<bool>(true);
|
2022-04-01 18:59:18 +02:00
|
|
|
static final _log = Logger("service._Service");
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Access localized string out of the main isolate
|
|
|
|
class _L10n {
|
|
|
|
factory _L10n() => _inst;
|
|
|
|
|
|
|
|
_L10n._();
|
|
|
|
|
|
|
|
Future<void> 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<AppLocalizations> _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 late final _inst = _L10n._();
|
|
|
|
late AppLocalizations _l10n;
|
|
|
|
|
|
|
|
static final _log = Logger("service._L10n");
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MetadataTask {
|
|
|
|
_MetadataTask(this.service, this.account, this.accountPref);
|
|
|
|
|
|
|
|
Future<void> call() async {
|
|
|
|
try {
|
|
|
|
await _updateMetadata();
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
_log.shout("[call] Uncaught exception", e, stackTrace);
|
|
|
|
}
|
|
|
|
if (_processedIds.isNotEmpty) {
|
|
|
|
NativeEvent.fire(FileExifUpdatedEvent(_processedIds).toEvent());
|
|
|
|
_processedIds = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _updateMetadata() async {
|
|
|
|
final shareFolder = File(
|
|
|
|
path: file_util.unstripPath(account, accountPref.getShareFolderOr()));
|
|
|
|
bool hasScanShareFolder = false;
|
|
|
|
final fileRepo = FileRepo(FileCachedDataSource(AppDb()));
|
|
|
|
for (final r in account.roots) {
|
|
|
|
final dir = File(path: file_util.unstripPath(account, r));
|
|
|
|
hasScanShareFolder |= file_util.isOrUnderDir(shareFolder, dir);
|
|
|
|
final updater = UpdateMissingMetadata(fileRepo);
|
2022-04-08 22:16:35 +02:00
|
|
|
void onServiceStop() {
|
|
|
|
_log.info("[_updateMetadata] Stopping task: user canceled");
|
|
|
|
updater.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
_Service._shouldRun.addListener(onServiceStop);
|
|
|
|
try {
|
|
|
|
await for (final ev in updater(account, dir)) {
|
|
|
|
if (ev is File) {
|
|
|
|
_onFileProcessed(ev);
|
|
|
|
}
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
2022-04-08 22:16:35 +02:00
|
|
|
} finally {
|
|
|
|
_Service._shouldRun.removeListener(onServiceStop);
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!hasScanShareFolder) {
|
|
|
|
final shareUpdater = UpdateMissingMetadata(fileRepo);
|
2022-04-08 22:16:35 +02:00
|
|
|
void onServiceStop() {
|
|
|
|
_log.info("[_updateMetadata] Stopping task: user canceled");
|
|
|
|
shareUpdater.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
_Service._shouldRun.addListener(onServiceStop);
|
|
|
|
try {
|
|
|
|
await for (final ev in shareUpdater(
|
|
|
|
account,
|
|
|
|
shareFolder,
|
|
|
|
isRecursive: false,
|
|
|
|
filter: (f) => f.ownerId != account.username,
|
|
|
|
)) {
|
|
|
|
if (ev is File) {
|
|
|
|
_onFileProcessed(ev);
|
|
|
|
}
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
2022-04-08 22:16:35 +02:00
|
|
|
} finally {
|
|
|
|
_Service._shouldRun.removeListener(onServiceStop);
|
2022-04-01 18:59:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 _count = 0;
|
|
|
|
var _processedIds = <int>[];
|
|
|
|
|
|
|
|
static final _log = Logger("service._MetadataTask");
|
|
|
|
}
|
|
|
|
|
|
|
|
final _log = Logger("service");
|