From e7170aca48070d8f97fcb1df83003105b61e7e32 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 27 Aug 2022 01:07:48 -0400 Subject: [PATCH] Various bugfixes + refactors --- lib/main.dart | 22 ++++--- lib/pages/add_app.dart | 25 ++++---- lib/pages/app.dart | 3 +- lib/pages/apps.dart | 15 +++-- lib/pages/settings.dart | 4 +- .../apps_provider.dart | 49 ++++++--------- .../notifications_provider.dart | 5 +- .../settings_provider.dart | 20 ++++++- .../source_provider.dart} | 60 ++++++++----------- 9 files changed, 111 insertions(+), 92 deletions(-) rename lib/{services => providers}/apps_provider.dart (83%) rename lib/{services => providers}/notifications_provider.dart (94%) rename lib/{services => providers}/settings_provider.dart (67%) rename lib/{services/source_service.dart => providers/source_provider.dart} (95%) diff --git a/lib/main.dart b/lib/main.dart index b6f7352..dd355fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:obtainium/pages/home.dart'; -import 'package:obtainium/services/apps_provider.dart'; -import 'package:obtainium/services/notifications_provider.dart'; -import 'package:obtainium/services/settings_provider.dart'; -import 'package:obtainium/services/source_service.dart'; +import 'package:obtainium/providers/apps_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:workmanager/workmanager.dart'; @@ -15,6 +15,7 @@ const String currentReleaseTag = @pragma('vm:entry-point') void bgTaskCallback() { + // Background update checking process Workmanager().executeTask((task, taskName) async { var appsProvider = AppsProvider(bg: true); var notificationsProvider = NotificationsProvider(); @@ -66,9 +67,16 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); AppsProvider appsProvider = context.read(); + if (settingsProvider.prefs == null) { - settingsProvider.initializeSettings(); + settingsProvider.initializeSettings().then((value) { + // Delete past downloads and check for updates every time the app is launched + // Only runs once as the settings are only initialized once (so not on every build) + appsProvider.deleteSavedAPKs(); + appsProvider.checkUpdates(); + }); } else { + // Register the background update task according to the user's setting Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check', frequency: Duration(minutes: settingsProvider.updateInterval), initialDelay: Duration(minutes: settingsProvider.updateInterval), @@ -76,6 +84,7 @@ class MyApp extends StatelessWidget { existingWorkPolicy: ExistingWorkPolicy.replace); bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); if (isFirstRun) { + // If this is the first run, ask for notification permissions and add Obtainium to the Apps list Permission.notification.request(); appsProvider.saveApp(App( 'imranr98_obtainium_github', @@ -85,12 +94,11 @@ class MyApp extends StatelessWidget { currentReleaseTag, currentReleaseTag, [])); } - appsProvider.deleteSavedAPKs(); - appsProvider.checkUpdates(); } 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 && diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index d85f11f..9cd16d2 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:obtainium/pages/app.dart'; -import 'package:obtainium/services/apps_provider.dart'; -import 'package:obtainium/services/source_service.dart'; +import 'package:obtainium/providers/apps_provider.dart'; +import 'package:obtainium/providers/settings_provider.dart'; +import 'package:obtainium/providers/source_provider.dart'; import 'package:provider/provider.dart'; class AddAppPage extends StatefulWidget { @@ -52,20 +53,24 @@ class _AddAppPageState extends State { setState(() { gettingAppInfo = true; }); - SourceService() + sourceProvider() .getApp(urlInputController.value.text) .then((app) { var appsProvider = context.read(); + var settingsProvider = + context.read(); if (appsProvider.apps.containsKey(app.id)) { throw 'App already added'; } - appsProvider.saveApp(app).then((_) { - urlInputController.clear(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppPage(appId: app.id))); + settingsProvider.getInstallPermission().then((_) { + appsProvider.saveApp(app).then((_) { + urlInputController.clear(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppPage(appId: app.id))); + }); }); }).catchError((e) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/pages/app.dart b/lib/pages/app.dart index f938549..08a3cb1 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:obtainium/services/apps_provider.dart'; +import 'package:obtainium/providers/apps_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:provider/provider.dart'; @@ -26,6 +26,7 @@ class _AppPageState extends State { ), body: WebView( initialUrl: app?.app.url, + javascriptMode: JavascriptMode.unrestricted, ), bottomSheet: Padding( padding: EdgeInsets.fromLTRB( diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 59790be..bf5253e 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:obtainium/pages/app.dart'; -import 'package:obtainium/services/apps_provider.dart'; +import 'package:obtainium/providers/apps_provider.dart'; +import 'package:obtainium/providers/settings_provider.dart'; import 'package:provider/provider.dart'; class AppsPage extends StatefulWidget { @@ -25,9 +26,15 @@ class _AppsPageState extends State { .isNotEmpty ? null : () { - for (var e in existingUpdateAppIds) { - appsProvider.downloadAndInstallLatestApp(e, context); - } + context + .read() + .getInstallPermission() + .then((_) { + for (var e in existingUpdateAppIds) { + appsProvider.downloadAndInstallLatestApp( + e, context); + } + }); }, icon: const Icon(Icons.update), label: const Text('Update All')), diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 57ec8e5..50e810d 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:obtainium/services/apps_provider.dart'; -import 'package:obtainium/services/settings_provider.dart'; +import 'package:obtainium/providers/apps_provider.dart'; +import 'package:obtainium/providers/settings_provider.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/services/apps_provider.dart b/lib/providers/apps_provider.dart similarity index 83% rename from lib/services/apps_provider.dart rename to lib/providers/apps_provider.dart index 9a017be..b70af35 100644 --- a/lib/services/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -1,20 +1,18 @@ -// Provider that manages App-related state and provides functions to retrieve App info download/install Apps +// Manages state related to the list of Apps tracked by Obtainium, +// Exposes related functions such as those used to add, remove, download, and install Apps. import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:obtainium/services/notifications_provider.dart'; +import 'package:obtainium/providers/notifications_provider.dart'; import 'package:provider/provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; -import 'package:obtainium/services/source_service.dart'; +import 'package:obtainium/providers/source_provider.dart'; import 'package:http/http.dart'; import 'package:install_plugin_v2/install_plugin_v2.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:fluttertoast/fluttertoast.dart'; class AppInMemory { late App app; @@ -31,18 +29,22 @@ class AppsProvider with ChangeNotifier { // Variables to keep track of the app foreground status (installs can't run in the background) bool isForeground = true; - StreamSubscription? foregroundSubscription; + late Stream foregroundStream; + late StreamSubscription foregroundSubscription; AppsProvider({bool bg = false}) { // Subscribe to changes in the app foreground status - foregroundSubscription = FGBGEvents.stream.listen((event) async { + foregroundStream = FGBGEvents.stream.asBroadcastStream(); + foregroundSubscription = foregroundStream.listen((event) async { isForeground = event == FGBGType.foreground; if (isForeground) await loadApps(); }); loadApps(); } - // Given a App (assumed valid), initiate an APK download (will trigger install callback when complete) + // Given an AppId, uses stored info about the app to download an APK (with user input if needed) and install it + // Installs can only be done in the foreground, so a notification is sent to get the user's attention if needed + // Returns upon successful download, regardless of installation result Future downloadAndInstallLatestApp( String appId, BuildContext context) async { var notificationsProvider = context.read(); @@ -107,10 +109,12 @@ class AppsProvider with ChangeNotifier { throw response.reasonPhrase ?? 'Unknown Error'; } - if (!isForeground) { + while (!isForeground) { await notificationsProvider.notify(completeInstallationNotification, cancelExisting: true); - while (await FGBGEvents.stream.first != FGBGType.foreground) { + if (await FGBGEvents.stream.first == FGBGType.foreground || + isForeground) { + break; // We need to wait for the App to come to the foreground to install it // Can't try to call install plugin in a background isolate (may not have worked anyways) because of: // https://github.com/flutter/flutter/issues/13937 @@ -120,16 +124,6 @@ class AppsProvider with ChangeNotifier { // Unfortunately this 'await' does not actually wait for the APK to finish installing // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing // This also does not use the 'session-based' installer API, so background/silent updates are impossible - while (!(await Permission.requestInstallPackages.isGranted)) { - // Explicit request as InstallPlugin request sometimes bugged - Fluttertoast.showToast( - msg: 'Please allow Obtainium to install Apps', - toastLength: Toast.LENGTH_LONG); - if ((await Permission.requestInstallPackages.request()) == - PermissionStatus.granted) { - break; - } - } await InstallPlugin.installApk(downloadFile.path, 'dev.imranr.obtainium'); apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion; @@ -199,7 +193,7 @@ class AppsProvider with ChangeNotifier { Future getUpdate(String appId) async { App? currentApp = apps[appId]!.app; - App newApp = await SourceService().getApp(currentApp.url); + App newApp = await sourceProvider().getApp(currentApp.url); if (newApp.latestVersion != currentApp.latestVersion) { newApp.installedVersion = currentApp.installedVersion; await saveApp(newApp); @@ -252,10 +246,7 @@ class AppsProvider with ChangeNotifier { } Future importApps(String appsJSON) async { - // FilePickerResult? result = await FilePicker.platform.pickFiles(); // Does not work on Android 13 - - // if (result != null) { - // String appsJSON = File(result.files.single.path!).readAsStringSync(); + // File picker does not work in android 13, so the user must paste the JSON directly into Obtainium to import Apps List importedApps = (jsonDecode(appsJSON) as List) .map((e) => App.fromJson(e)) .toList(); @@ -266,15 +257,11 @@ class AppsProvider with ChangeNotifier { } notifyListeners(); return importedApps.length; - // } else { - // User canceled the picker - // } } @override void dispose() { - IsolateNameServer.removePortNameMapping('downloader_send_port'); - foregroundSubscription?.cancel(); + foregroundSubscription.cancel(); super.dispose(); } } diff --git a/lib/services/notifications_provider.dart b/lib/providers/notifications_provider.dart similarity index 94% rename from lib/services/notifications_provider.dart rename to lib/providers/notifications_provider.dart index efadf1e..de0fdd6 100644 --- a/lib/services/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -1,5 +1,8 @@ +// Exposes functions that can be used to send notifications to the user +// Contains a set of pre-defined ObtainiumNotification objects that should be used throughout the app + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:obtainium/services/source_service.dart'; +import 'package:obtainium/providers/source_provider.dart'; class ObtainiumNotification { late int id; diff --git a/lib/services/settings_provider.dart b/lib/providers/settings_provider.dart similarity index 67% rename from lib/services/settings_provider.dart rename to lib/providers/settings_provider.dart index eddee41..ed78fc7 100644 --- a/lib/services/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,4 +1,8 @@ +// Exposes functions used to save/load app settings + import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum ThemeSettings { system, light, dark } @@ -22,7 +26,6 @@ class SettingsProvider with ChangeNotifier { } set theme(ThemeSettings t) { - print(t); prefs?.setInt('theme', t.index); notifyListeners(); } @@ -46,11 +49,24 @@ class SettingsProvider with ChangeNotifier { notifyListeners(); } - checkAndFlipFirstRun() { + bool checkAndFlipFirstRun() { bool result = prefs?.getBool('firstRun') ?? true; if (result) { prefs?.setBool('firstRun', false); } return result; } + + Future getInstallPermission() async { + while (!(await Permission.requestInstallPackages.isGranted)) { + // Explicit request as InstallPlugin request sometimes bugged + Fluttertoast.showToast( + msg: 'Please allow Obtainium to install Apps', + toastLength: Toast.LENGTH_LONG); + if ((await Permission.requestInstallPackages.request()) == + PermissionStatus.granted) { + break; + } + } + } } diff --git a/lib/services/source_service.dart b/lib/providers/source_provider.dart similarity index 95% rename from lib/services/source_service.dart rename to lib/providers/source_provider.dart index 4fb0d76..ef59727 100644 --- a/lib/services/source_service.dart +++ b/lib/providers/source_provider.dart @@ -1,5 +1,5 @@ -// Exposes functions related to interacting with App sources and retrieving App info -// Stateless - not a provider +// Defines App sources and provides functions used to interact with them +// AppSource is an abstract class with a concrete implementation for each source import 'dart:convert'; @@ -7,8 +7,6 @@ import 'package:html/dom.dart'; import 'package:http/http.dart'; import 'package:html/parser.dart'; -// Sub-classes used in App Source - class AppNames { late String author; late String name; @@ -23,34 +21,6 @@ class APKDetails { APKDetails(this.version, this.apkUrls); } -// App Source abstract class (diff. implementations for GitHub, GitLab, etc.) - -abstract class AppSource { - late String sourceId; - String standardizeURL(String url); - Future getLatestAPKDetails(String standardUrl); - AppNames getAppNames(String standardUrl); -} - -escapeRegEx(String s) { - return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) { - return "\\${x[0]}"; - }); -} - -List getLinksFromParsedHTML( - Document dom, RegExp hrefPattern, String prependToLinks) => - dom - .querySelectorAll('a') - .where((element) { - if (element.attributes['href'] == null) return false; - return hrefPattern.hasMatch(element.attributes['href']!); - }) - .map((e) => '$prependToLinks${e.attributes['href']!}') - .toList(); - -// App class - class App { late String id; late String url; @@ -89,7 +59,29 @@ class App { }; } -// Specific App Source classes +escapeRegEx(String s) { + return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) { + return "\\${x[0]}"; + }); +} + +List getLinksFromParsedHTML( + Document dom, RegExp hrefPattern, String prependToLinks) => + dom + .querySelectorAll('a') + .where((element) { + if (element.attributes['href'] == null) return false; + return hrefPattern.hasMatch(element.attributes['href']!); + }) + .map((e) => '$prependToLinks${e.attributes['href']!}') + .toList(); + +abstract class AppSource { + late String sourceId; + String standardizeURL(String url); + Future getLatestAPKDetails(String standardUrl); + AppNames getAppNames(String standardUrl); +} class GitHub implements AppSource { @override @@ -203,7 +195,7 @@ class GitLab implements AppSource { } } -class SourceService { +class sourceProvider { // Add more source classes here so they are available via the service AppSource getSource(String url) { if (url.toLowerCase().contains('://github.com')) {