import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/pages/home.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; import 'package:easy_localization/easy_localization.dart'; // ignore: implementation_imports import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; const String currentVersion = '0.13.27'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES const int bgUpdateCheckAlarmId = 666; List> supportedLocales = const [ MapEntry(Locale('en'), 'English'), MapEntry(Locale('zh'), '汉语'), MapEntry(Locale('it'), 'Italiano'), MapEntry(Locale('ja'), '日本語'), MapEntry(Locale('hu'), 'Magyar'), MapEntry(Locale('de'), 'Deutsch'), MapEntry(Locale('fa'), 'فارسی'), MapEntry(Locale('fr'), 'Français'), MapEntry(Locale('es'), 'Español'), MapEntry(Locale('pl'), 'Polski'), MapEntry(Locale('ru'), 'Русский язык'), MapEntry(Locale('bs'), 'Bosanski'), ]; const fallbackLocale = Locale('en'); const localeDir = 'assets/translations'; final globalNavigatorKey = GlobalKey(); Future loadTranslations() async { // See easy_localization/issues/210 await EasyLocalizationController.initEasyLocation(); var s = SettingsProvider(); await s.initializeSettings(); var forceLocale = s.forcedLocale; final controller = EasyLocalizationController( saveLocale: true, forceLocale: forceLocale != null ? Locale(forceLocale) : null, fallbackLocale: fallbackLocale, supportedLocales: supportedLocales.map((e) => e.key).toList(), assetLoader: const RootBundleAssetLoader(), useOnlyLangCode: true, useFallbackTranslations: true, path: localeDir, onLoadError: (FlutterError e) { throw e; }, ); await controller.loadTranslations(); Localization.load(controller.locale, translations: controller.translations, fallbackTranslations: controller.fallbackTranslations); } moveStrToEnd(List arr, String str, {String? strB}) { String? temp; arr.removeWhere((element) { bool res = element == str || element == strB; if (res) { temp = element; } return res; }); if (temp != null) { arr = [...arr, temp!]; } return arr; } /// Background updater function /// /// @param List? toCheck: The appIds to check for updates (default to all apps sorted by last update check time) /// /// @param List? toInstall: The appIds to attempt to update (defaults to an empty array) /// /// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0) /// /// When toCheck is empty, the function is in "install mode" (else it is in "update mode"). /// In update mode, all apps in toCheck are checked for updates. /// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified. /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. /// /// Once all update checks are complete, the function is called again in install mode. /// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown). /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. /// /// In either mode, if the function fails after the maximum number of tries, the user is notified. @pragma('vm:entry-point') Future bgUpdateCheck(int taskId, Map? params) async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); await AndroidAlarmManager.initialize(); await loadTranslations(); LogsProvider logs = LogsProvider(); NotificationsProvider notificationsProvider = NotificationsProvider(); AppsProvider appsProvider = AppsProvider(isBg: true); await appsProvider.loadApps(); var settingsProvider = SettingsProvider(); await settingsProvider.initializeSettings(); int maxAttempts = 5; params ??= {}; if (params['toCheck'] == null) { settingsProvider.lastBGCheckTime = DateTime.now(); } int attemptCount = (params['attemptCount'] ?? 0) + 1; List toCheck = [ ...(params['toCheck'] ?? appsProvider.getAppsSortedByUpdateCheckTime()) ]; List toInstall = [...(params['toInstall'] ?? ([]))]; bool installMode = toCheck.isEmpty && toInstall.isNotEmpty; logs.add( 'BG update task $taskId: Started in ${installMode ? 'install' : 'update'} mode${attemptCount > 1 ? '. ${attemptCount - 1} consecutive fail(s)' : ''}.'); if (!installMode) { var didCompleteChecking = false; for (int i = 0; i < toCheck.length; i++) { var appId = toCheck[i]; AppInMemory? app = appsProvider.apps[appId]; if (app?.app.installedVersion != null) { try { logs.add('BG update task $taskId: Checking for updates for $appId.'); notificationsProvider.notify(checkingUpdatesNotification, cancelExisting: true); App? newApp = await appsProvider.checkUpdate(appId); if (newApp != null) { if (!(await appsProvider.canInstallSilently( app!.app, settingsProvider))) { notificationsProvider.notify( UpdateNotification([newApp], id: newApp.id.hashCode - 1)); } else { toInstall.add(appId); } } if (i == (toCheck.length - 1)) { didCompleteChecking = true; } } catch (e) { logs.add( 'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.'); if (attemptCount < maxAttempts) { var remainingSeconds = e is RateLimitError ? (e.remainingMinutes * 60) : e is ClientException ? (15 * 60) : 1; logs.add( 'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); var remainingToCheck = moveStrToEnd(toCheck.sublist(i), appId); AndroidAlarmManager.oneShot( Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, params: { 'toCheck': remainingToCheck, 'toInstall': toInstall, 'attemptCount': attemptCount }); break; } else { notificationsProvider .notify(ErrorCheckingUpdatesNotification(e.toString())); } } finally { notificationsProvider.cancel(checkingUpdatesNotification.id); } } } if (didCompleteChecking && toInstall.isNotEmpty) { AndroidAlarmManager.oneShot( const Duration(minutes: 0), taskId + 1, bgUpdateCheck, params: {'toCheck': [], 'toInstall': toInstall}); } } else { toInstall = moveStrToEnd(toInstall, obtainiumId); for (var i = 0; i < toInstall.length; i++) { String appId = toInstall[i]; try { logs.add( 'BG update task $taskId: Attempting to update $appId in the background.'); await appsProvider.downloadAndInstallLatestApps( [appId], null, settingsProvider, notificationsProvider: notificationsProvider); } catch (e) { logs.add( 'BG update task $taskId: Got error on updating $appId \'${e.toString()}\'.'); if (attemptCount < maxAttempts) { var remainingSeconds = 1; logs.add( 'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); var remainingToInstall = moveStrToEnd(toInstall.sublist(i), appId); AndroidAlarmManager.oneShot( Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, params: { 'toCheck': toCheck, 'toInstall': remainingToInstall, 'attemptCount': attemptCount }); break; } else { notificationsProvider .notify(ErrorCheckingUpdatesNotification(e.toString())); } } } } } void main() async { WidgetsFlutterBinding.ensureInitialized(); try { ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); SecurityContext.defaultContext .setTrustedCertificatesBytes(data.buffer.asUint8List()); } catch (e) { // Already added, do nothing (see #375) } await EasyLocalization.ensureInitialized(); if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), ); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } await AndroidAlarmManager.initialize(); runApp(MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => AppsProvider()), ChangeNotifierProvider(create: (context) => SettingsProvider()), Provider(create: (context) => NotificationsProvider()), Provider(create: (context) => LogsProvider()) ], child: EasyLocalization( supportedLocales: supportedLocales.map((e) => e.key).toList(), path: localeDir, fallbackLocale: fallbackLocale, useOnlyLangCode: true, child: const Obtainium()), )); } var defaultThemeColour = Colors.deepPurple; class Obtainium extends StatefulWidget { const Obtainium({super.key}); @override State createState() => _ObtainiumState(); } class _ObtainiumState extends State { var existingUpdateInterval = -1; @override Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); AppsProvider appsProvider = context.read(); LogsProvider logs = context.read(); if (settingsProvider.prefs == null) { settingsProvider.initializeSettings(); } else { bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); if (isFirstRun) { logs.add('This is the first ever run of Obtainium.'); // If this is the first run, ask for notification permissions and add Obtainium to the Apps list Permission.notification.request(); appsProvider.saveApps([ App( obtainiumId, 'https://github.com/ImranR98/Obtainium', 'ImranR98', 'Obtainium', currentReleaseTag, currentReleaseTag, [], 0, {'includePrereleases': true}, null, false) ], onlyIfExists: false); } if (!supportedLocales .map((e) => e.key.languageCode) .contains(context.locale.languageCode) || (settingsProvider.forcedLocale == null && context.deviceLocale.languageCode != context.locale.languageCode)) { settingsProvider.resetLocaleSafe(context); } // Register the background update task according to the user's setting var actualUpdateInterval = settingsProvider.updateInterval; if (existingUpdateInterval != actualUpdateInterval) { if (actualUpdateInterval == 0) { AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); } else { var settingChanged = existingUpdateInterval != -1; var lastCheckWasTooLongAgo = actualUpdateInterval != 0 && settingsProvider.lastBGCheckTime .add(Duration(minutes: actualUpdateInterval + 60)) .isBefore(DateTime.now()); if (settingChanged || lastCheckWasTooLongAgo) { logs.add( 'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).'); AndroidAlarmManager.periodic( Duration(minutes: actualUpdateInterval), bgUpdateCheckAlarmId, bgUpdateCheck, rescheduleOnReboot: true, wakeup: true); } } existingUpdateInterval = actualUpdateInterval; } } return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { // Decide on a colour/brightness scheme based on OS and user settings ColorScheme lightColorScheme; ColorScheme darkColorScheme; if (lightDynamic != null && darkDynamic != null && settingsProvider.colour == ColourSettings.materialYou) { lightColorScheme = lightDynamic.harmonized(); darkColorScheme = darkDynamic.harmonized(); } else { lightColorScheme = ColorScheme.fromSeed(seedColor: defaultThemeColour); darkColorScheme = ColorScheme.fromSeed( seedColor: defaultThemeColour, brightness: Brightness.dark); } // set the background and surface colors to pure black in the amoled theme if (settingsProvider.useBlackTheme) { darkColorScheme = darkColorScheme .copyWith(background: Colors.black, surface: Colors.black) .harmonized(); } return MaterialApp( title: 'Obtainium', localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, navigatorKey: globalNavigatorKey, theme: ThemeData( useMaterial3: true, colorScheme: settingsProvider.theme == ThemeSettings.dark ? darkColorScheme : lightColorScheme, fontFamily: 'Metropolis'), darkTheme: ThemeData( useMaterial3: true, colorScheme: settingsProvider.theme == ThemeSettings.light ? lightColorScheme : darkColorScheme, fontFamily: 'Metropolis'), home: Shortcuts(shortcuts: { LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), }, child: const HomePage())); }); } }