import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.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.11.15'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES const int bgUpdateCheckAlarmId = 666; const supportedLocales = [ Locale('en'), Locale('zh'), Locale('it'), Locale('ja'), Locale('hu'), Locale('de'), Locale('fa'), Locale('fr') ]; 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, 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); } @pragma('vm:entry-point') Future bgUpdateCheck(int taskId, Map? params) async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); await loadTranslations(); LogsProvider logs = LogsProvider(); logs.add(tr('startedBgUpdateTask')); int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds']; await AndroidAlarmManager.initialize(); DateTime? ignoreAfter = ignoreAfterMicroseconds != null ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) : null; logs.add(tr('bgUpdateIgnoreAfterIs', args: [ignoreAfter.toString()])); var notificationsProvider = NotificationsProvider(); await notificationsProvider.notify(checkingUpdatesNotification); try { var appsProvider = AppsProvider(); await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id); await appsProvider.loadApps(); List existingUpdateIds = appsProvider.findExistingUpdates(installedOnly: true); DateTime nextIgnoreAfter = DateTime.now(); String? err; try { logs.add(tr('startedActualBGUpdateCheck')); await appsProvider.checkUpdates( ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true); } catch (e) { if (e is RateLimitError || e is SocketException) { var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15; logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes, args: [e.toString(), remainingMinutes.toString()])); AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes), Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: { 'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch }); } else { err = e.toString(); } } List newUpdates = appsProvider .findExistingUpdates(installedOnly: true) .where((id) => !existingUpdateIds.contains(id)) .map((e) => appsProvider.apps[e]!.app) .toList(); // TODO: This silent update code doesn't work yet // List silentlyUpdated = await appsProvider // .downloadAndInstallLatestApp( // [...newUpdates.map((e) => e.id), ...existingUpdateIds], null); // if (silentlyUpdated.isNotEmpty) { // newUpdates = newUpdates // .where((element) => !silentlyUpdated.contains(element.id)) // .toList(); // notificationsProvider.notify( // SilentUpdateNotification( // silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()), // cancelExisting: true); // } logs.add( plural('bgCheckFoundUpdatesWillNotifyIfNeeded', newUpdates.length)); if (newUpdates.isNotEmpty) { notificationsProvider.notify(UpdateNotification(newUpdates)); } if (err != null) { throw err; } } catch (e) { notificationsProvider .notify(ErrorCheckingUpdatesNotification(e.toString())); } finally { logs.add(tr('bgUpdateTaskFinished')); await notificationsProvider.cancel(checkingUpdatesNotification.id); } } 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, 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(tr('firstRun')); // 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.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 if (existingUpdateInterval != settingsProvider.updateInterval) { if (existingUpdateInterval != -1) { logs.add(tr('settingUpdateCheckIntervalTo', args: [settingsProvider.updateInterval.toString()])); } existingUpdateInterval = settingsProvider.updateInterval; if (existingUpdateInterval == 0) { AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); } else { AndroidAlarmManager.periodic( Duration(minutes: existingUpdateInterval), bgUpdateCheckAlarmId, bgUpdateCheck, rescheduleOnReboot: true, wakeup: true); } } } 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); } 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: const HomePage()); }); } }