123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- import 'dart:async';
- import 'dart:io';
- import 'dart:isolate';
- import 'package:background_fetch/background_fetch.dart';
- import 'package:firebase_messaging/firebase_messaging.dart';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_easyloading/flutter_easyloading.dart';
- import 'package:flutter_gen/gen_l10n/app_localizations.dart';
- import 'package:flutter_localizations/flutter_localizations.dart';
- import 'package:in_app_purchase/in_app_purchase.dart';
- import 'package:logging/logging.dart';
- import 'package:path_provider/path_provider.dart';
- import 'package:photos/core/configuration.dart';
- import 'package:photos/core/constants.dart';
- import 'package:photos/core/network.dart';
- import 'package:photos/db/upload_locks_db.dart';
- import 'package:photos/l10n/l10n.dart';
- import 'package:photos/services/app_lifecycle_service.dart';
- import 'package:photos/services/billing_service.dart';
- import 'package:photos/services/collections_service.dart';
- import 'package:photos/services/feature_flag_service.dart';
- import 'package:photos/services/local_sync_service.dart';
- import 'package:photos/services/memories_service.dart';
- import 'package:photos/services/notification_service.dart';
- import 'package:photos/services/push_service.dart';
- import 'package:photos/services/remote_sync_service.dart';
- import 'package:photos/services/sync_service.dart';
- import 'package:photos/services/trash_sync_service.dart';
- import 'package:photos/services/update_service.dart';
- import 'package:photos/ui/app_lock.dart';
- import 'package:photos/ui/home_widget.dart';
- import 'package:photos/ui/lock_screen.dart';
- import 'package:photos/utils/crypto_util.dart';
- import 'package:photos/utils/file_uploader.dart';
- import 'package:photos/utils/local_settings.dart';
- import 'package:shared_preferences/shared_preferences.dart';
- import 'package:super_logging/super_logging.dart';
- import 'package:visibility_detector/visibility_detector.dart';
- final _logger = Logger("main");
- Completer<void> _initializationStatus;
- const kLastBGTaskHeartBeatTime = "bg_task_hb_time";
- const kLastFGTaskHeartBeatTime = "fg_task_hb_time";
- const kHeartBeatFrequency = Duration(seconds: 1);
- const kFGSyncFrequency = Duration(minutes: 5);
- const kBGTaskTimeout = Duration(seconds: 25);
- const kBGPushTimeout = Duration(seconds: 28);
- const kFGTaskDeathTimeoutInMicroseconds = 5000000;
- final themeData = ThemeData(
- fontFamily: 'Ubuntu',
- brightness: Brightness.dark,
- hintColor: Colors.grey,
- accentColor: Color.fromRGBO(45, 194, 98, 0.2),
- buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
- buttonTheme: ButtonThemeData().copyWith(
- buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
- ),
- toggleableActiveColor: Colors.green[400],
- scaffoldBackgroundColor: Colors.black,
- backgroundColor: Colors.black,
- appBarTheme: AppBarTheme().copyWith(
- color: Color.fromRGBO(10, 20, 20, 1.0),
- ),
- cardColor: Color.fromRGBO(10, 15, 15, 1.0),
- dialogTheme: DialogTheme().copyWith(
- backgroundColor: Color.fromRGBO(10, 15, 15, 1.0),
- ),
- textSelectionTheme: TextSelectionThemeData().copyWith(
- cursorColor: Colors.white.withOpacity(0.5),
- ),
- inputDecorationTheme: InputDecorationTheme().copyWith(
- focusedBorder: UnderlineInputBorder(
- borderSide: BorderSide(
- color: Color.fromRGBO(45, 194, 98, 1.0),
- ),
- ),
- ),
- );
- void main() async {
- WidgetsFlutterBinding.ensureInitialized();
- await _runInForeground();
- BackgroundFetch.registerHeadlessTask(_headlessTaskHandler);
- }
- Future<void> _runInForeground() async {
- return await _runWithLogs(() async {
- _logger.info("Starting app in foreground");
- await _init(false);
- _scheduleFGSync();
- runApp(AppLock(
- builder: (args) => EnteApp(),
- lockScreen: LockScreen(),
- enabled: Configuration.instance.shouldShowLockScreen(),
- themeData: themeData,
- ));
- });
- }
- Future _runInBackground(String taskId) async {
- if (_initializationStatus == null) {
- _runWithLogs(() async {
- _backgroundTask(taskId);
- }, prefix: "[bg]");
- } else {
- _backgroundTask(taskId);
- }
- }
- void _backgroundTask(String taskId) async {
- await Future.delayed(Duration(seconds: 3));
- if (await _isRunningInForeground()) {
- _logger.info("FG task running, skipping BG task");
- BackgroundFetch.finish(taskId);
- return;
- } else {
- _logger.info("FG task is not running");
- }
- _logger.info("[BackgroundFetch] Event received: $taskId");
- _scheduleBGTaskKill(taskId);
- if (Platform.isIOS) {
- _scheduleSuicide(kBGTaskTimeout); // To prevent OS from punishing us
- }
- await _init(true);
- UpdateService.instance.showUpdateNotification();
- await _sync();
- BackgroundFetch.finish(taskId);
- }
- void _headlessTaskHandler(HeadlessTask task) {
- if (task.timeout) {
- BackgroundFetch.finish(task.taskId);
- } else {
- _runInBackground(task.taskId);
- }
- }
- Future<void> _init(bool isBackground) async {
- if (_initializationStatus != null) {
- return _initializationStatus.future;
- }
- _initializationStatus = Completer<void>();
- _logger.info("Initializing...");
- _scheduleHeartBeat(isBackground);
- if (isBackground) {
- AppLifecycleService.instance.onAppInBackground();
- } else {
- AppLifecycleService.instance.onAppInForeground();
- }
- InAppPurchaseConnection.enablePendingPurchases();
- CryptoUtil.init();
- await NotificationService.instance.init();
- await Network.instance.init();
- await Configuration.instance.init();
- await UpdateService.instance.init();
- await BillingService.instance.init();
- await CollectionsService.instance.init();
- await FileUploader.instance.init(isBackground);
- await LocalSyncService.instance.init();
- await TrashSyncService.instance.init();
- await RemoteSyncService.instance.init();
- await SyncService.instance.init();
- await MemoriesService.instance.init();
- await LocalSettings.instance.init();
- PushService.instance.init().then((_) {
- FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
- });
- FeatureFlagService.instance.init();
- _logger.info("Initialization done");
- _initializationStatus.complete();
- }
- Future<void> _sync() async {
- if (SyncService.instance.isSyncInProgress()) {
- _logger.info("Sync is already in progress, skipping");
- return;
- }
- if (!AppLifecycleService.instance.isForeground) {
- _logger.info("Syncing in background");
- }
- try {
- await SyncService.instance.sync();
- } catch (e, s) {
- _logger.severe("Sync error", e, s);
- }
- }
- Future _runWithLogs(Function() function, {String prefix = ""}) async {
- await SuperLogging.main(LogConfig(
- body: function,
- logDirPath: (await getTemporaryDirectory()).path + "/logs",
- maxLogFiles: 5,
- sentryDsn: kDebugMode ? kSentryDebugDSN : kSentryDSN,
- enableInDebugMode: true,
- prefix: prefix,
- ));
- }
- Future<void> _scheduleHeartBeat(bool isBackground) async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.setInt(
- isBackground ? kLastBGTaskHeartBeatTime : kLastFGTaskHeartBeatTime,
- DateTime.now().microsecondsSinceEpoch);
- Future.delayed(kHeartBeatFrequency, () async {
- _scheduleHeartBeat(isBackground);
- });
- }
- Future<void> _scheduleFGSync() async {
- await _sync();
- Future.delayed(kFGSyncFrequency, () async {
- _scheduleFGSync();
- });
- }
- void _scheduleBGTaskKill(String taskId) async {
- if (await _isRunningInForeground()) {
- _logger.info("Found app in FG, committing seppuku.");
- await _killBGTask(taskId);
- return;
- }
- Future.delayed(kHeartBeatFrequency, () async {
- _scheduleBGTaskKill(taskId);
- });
- }
- Future<bool> _isRunningInForeground() async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.reload();
- final currentTime = DateTime.now().microsecondsSinceEpoch;
- return (prefs.getInt(kLastFGTaskHeartBeatTime) ?? 0) >
- (currentTime - kFGTaskDeathTimeoutInMicroseconds);
- }
- Future<void> _killBGTask([String taskId]) async {
- await UploadLocksDB.instance.releaseLocksAcquiredByOwnerBefore(
- ProcessType.background.toString(), DateTime.now().microsecondsSinceEpoch);
- final prefs = await SharedPreferences.getInstance();
- prefs.remove(kLastBGTaskHeartBeatTime);
- if (taskId != null) {
- BackgroundFetch.finish(taskId);
- }
- Isolate.current.kill(priority: Isolate.immediate);
- }
- class EnteApp extends StatefulWidget {
- static const _homeWidget = HomeWidget();
- @override
- _EnteAppState createState() => _EnteAppState();
- }
- class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- _configureBackgroundFetch();
- }
- @override
- Widget build(BuildContext context) {
- return VisibilityDetector(
- key: GlobalKey(),
- onVisibilityChanged: (visibility) {
- if (visibility.visibleFraction == 1) {
- Logger("EnteApp").info("App is in foreground");
- AppLifecycleService.instance.onAppInForeground();
- } else {
- Logger("EnteApp").info("App is in background");
- AppLifecycleService.instance.onAppInBackground();
- }
- },
- child: MaterialApp(
- title: "ente",
- theme: themeData,
- home: EnteApp._homeWidget,
- debugShowCheckedModeBanner: false,
- navigatorKey: Network.instance.getAlice().getNavigatorKey(),
- builder: EasyLoading.init(),
- supportedLocales: L10n.all,
- localizationsDelegates: const [
- AppLocalizations.delegate,
- GlobalMaterialLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- ],
- ),
- );
- }
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {
- if (state == AppLifecycleState.resumed) {
- AppLifecycleService.instance.onAppInForeground();
- _sync();
- } else {
- AppLifecycleService.instance.onAppInBackground();
- }
- }
- void _configureBackgroundFetch() {
- BackgroundFetch.configure(
- BackgroundFetchConfig(
- minimumFetchInterval: 15,
- forceAlarmManager: false,
- stopOnTerminate: false,
- startOnBoot: true,
- enableHeadless: true,
- requiresBatteryNotLow: false,
- requiresCharging: false,
- requiresStorageNotLow: false,
- requiresDeviceIdle: false,
- requiredNetworkType: NetworkType.NONE,
- ), (String taskId) async {
- await _runInBackground(taskId);
- }, (taskId) {
- _logger.info("BG task timeout");
- _killBGTask(taskId);
- }).then((int status) {
- _logger.info('[BackgroundFetch] configure success: $status');
- }).catchError((e) {
- _logger.info('[BackgroundFetch] configure ERROR: $e');
- });
- }
- }
- Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
- if (_initializationStatus == null) {
- // App is dead
- _runWithLogs(() async {
- _logger.info("Background push received");
- if (Platform.isIOS) {
- _scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
- }
- await _init(true);
- if (PushService.shouldSync(message)) {
- await _sync();
- }
- }, prefix: "[bg]");
- } else {
- _logger.info("Background push received when app is alive");
- if (PushService.shouldSync(message)) {
- await _sync();
- }
- }
- }
- void _scheduleSuicide(Duration duration, [String taskID]) {
- Future.delayed(duration, () {
- _logger.warning("TLE");
- _killBGTask(taskID);
- });
- }
|