Various bugfixes + refactors

This commit is contained in:
Imran Remtulla
2022-08-27 01:07:48 -04:00
parent 7932b909c0
commit e7170aca48
9 changed files with 111 additions and 92 deletions

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:obtainium/pages/home.dart'; import 'package:obtainium/pages/home.dart';
import 'package:obtainium/services/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/services/notifications_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart';
import 'package:obtainium/services/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/services/source_service.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
@@ -15,6 +15,7 @@ const String currentReleaseTag =
@pragma('vm:entry-point') @pragma('vm:entry-point')
void bgTaskCallback() { void bgTaskCallback() {
// Background update checking process
Workmanager().executeTask((task, taskName) async { Workmanager().executeTask((task, taskName) async {
var appsProvider = AppsProvider(bg: true); var appsProvider = AppsProvider(bg: true);
var notificationsProvider = NotificationsProvider(); var notificationsProvider = NotificationsProvider();
@@ -66,9 +67,16 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
SettingsProvider settingsProvider = context.watch<SettingsProvider>(); SettingsProvider settingsProvider = context.watch<SettingsProvider>();
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
if (settingsProvider.prefs == null) { 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 { } else {
// Register the background update task according to the user's setting
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check', Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
frequency: Duration(minutes: settingsProvider.updateInterval), frequency: Duration(minutes: settingsProvider.updateInterval),
initialDelay: Duration(minutes: settingsProvider.updateInterval), initialDelay: Duration(minutes: settingsProvider.updateInterval),
@@ -76,6 +84,7 @@ class MyApp extends StatelessWidget {
existingWorkPolicy: ExistingWorkPolicy.replace); existingWorkPolicy: ExistingWorkPolicy.replace);
bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
if (isFirstRun) { if (isFirstRun) {
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
Permission.notification.request(); Permission.notification.request();
appsProvider.saveApp(App( appsProvider.saveApp(App(
'imranr98_obtainium_github', 'imranr98_obtainium_github',
@@ -85,12 +94,11 @@ class MyApp extends StatelessWidget {
currentReleaseTag, currentReleaseTag,
currentReleaseTag, [])); currentReleaseTag, []));
} }
appsProvider.deleteSavedAPKs();
appsProvider.checkUpdates();
} }
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
// Decide on a colour/brightness scheme based on OS and user settings
ColorScheme lightColorScheme; ColorScheme lightColorScheme;
ColorScheme darkColorScheme; ColorScheme darkColorScheme;
if (lightDynamic != null && if (lightDynamic != null &&

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:obtainium/pages/app.dart'; import 'package:obtainium/pages/app.dart';
import 'package:obtainium/services/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/services/source_service.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AddAppPage extends StatefulWidget { class AddAppPage extends StatefulWidget {
@@ -52,20 +53,24 @@ class _AddAppPageState extends State<AddAppPage> {
setState(() { setState(() {
gettingAppInfo = true; gettingAppInfo = true;
}); });
SourceService() sourceProvider()
.getApp(urlInputController.value.text) .getApp(urlInputController.value.text)
.then((app) { .then((app) {
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.read<AppsProvider>();
var settingsProvider =
context.read<SettingsProvider>();
if (appsProvider.apps.containsKey(app.id)) { if (appsProvider.apps.containsKey(app.id)) {
throw 'App already added'; throw 'App already added';
} }
appsProvider.saveApp(app).then((_) { settingsProvider.getInstallPermission().then((_) {
urlInputController.clear(); appsProvider.saveApp(app).then((_) {
Navigator.push( urlInputController.clear();
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => MaterialPageRoute(
AppPage(appId: app.id))); builder: (context) =>
AppPage(appId: app.id)));
});
}); });
}).catchError((e) { }).catchError((e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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:webview_flutter/webview_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -26,6 +26,7 @@ class _AppPageState extends State<AppPage> {
), ),
body: WebView( body: WebView(
initialUrl: app?.app.url, initialUrl: app?.app.url,
javascriptMode: JavascriptMode.unrestricted,
), ),
bottomSheet: Padding( bottomSheet: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:obtainium/pages/app.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'; import 'package:provider/provider.dart';
class AppsPage extends StatefulWidget { class AppsPage extends StatefulWidget {
@@ -25,9 +26,15 @@ class _AppsPageState extends State<AppsPage> {
.isNotEmpty .isNotEmpty
? null ? null
: () { : () {
for (var e in existingUpdateAppIds) { context
appsProvider.downloadAndInstallLatestApp(e, context); .read<SettingsProvider>()
} .getInstallPermission()
.then((_) {
for (var e in existingUpdateAppIds) {
appsProvider.downloadAndInstallLatestApp(
e, context);
}
});
}, },
icon: const Icon(Icons.update), icon: const Icon(Icons.update),
label: const Text('Update All')), label: const Text('Update All')),

View File

@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:obtainium/services/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/services/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';

View File

@@ -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:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart'; 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:provider/provider.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter_fgbg/flutter_fgbg.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:http/http.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart'; import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart';
class AppInMemory { class AppInMemory {
late App app; 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) // Variables to keep track of the app foreground status (installs can't run in the background)
bool isForeground = true; bool isForeground = true;
StreamSubscription<FGBGType>? foregroundSubscription; late Stream<FGBGType> foregroundStream;
late StreamSubscription<FGBGType> foregroundSubscription;
AppsProvider({bool bg = false}) { AppsProvider({bool bg = false}) {
// Subscribe to changes in the app foreground status // 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; isForeground = event == FGBGType.foreground;
if (isForeground) await loadApps(); if (isForeground) await loadApps();
}); });
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<void> downloadAndInstallLatestApp( Future<void> downloadAndInstallLatestApp(
String appId, BuildContext context) async { String appId, BuildContext context) async {
var notificationsProvider = context.read<NotificationsProvider>(); var notificationsProvider = context.read<NotificationsProvider>();
@@ -107,10 +109,12 @@ class AppsProvider with ChangeNotifier {
throw response.reasonPhrase ?? 'Unknown Error'; throw response.reasonPhrase ?? 'Unknown Error';
} }
if (!isForeground) { while (!isForeground) {
await notificationsProvider.notify(completeInstallationNotification, await notificationsProvider.notify(completeInstallationNotification,
cancelExisting: true); 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 // 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: // 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 // 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 // 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 // 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 // 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'); await InstallPlugin.installApk(downloadFile.path, 'dev.imranr.obtainium');
apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion; apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion;
@@ -199,7 +193,7 @@ class AppsProvider with ChangeNotifier {
Future<App?> getUpdate(String appId) async { Future<App?> getUpdate(String appId) async {
App? currentApp = apps[appId]!.app; App? currentApp = apps[appId]!.app;
App newApp = await SourceService().getApp(currentApp.url); App newApp = await sourceProvider().getApp(currentApp.url);
if (newApp.latestVersion != currentApp.latestVersion) { if (newApp.latestVersion != currentApp.latestVersion) {
newApp.installedVersion = currentApp.installedVersion; newApp.installedVersion = currentApp.installedVersion;
await saveApp(newApp); await saveApp(newApp);
@@ -252,10 +246,7 @@ class AppsProvider with ChangeNotifier {
} }
Future<int> importApps(String appsJSON) async { Future<int> importApps(String appsJSON) async {
// FilePickerResult? result = await FilePicker.platform.pickFiles(); // Does not work on Android 13 // File picker does not work in android 13, so the user must paste the JSON directly into Obtainium to import Apps
// if (result != null) {
// String appsJSON = File(result.files.single.path!).readAsStringSync();
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>) List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
.map((e) => App.fromJson(e)) .map((e) => App.fromJson(e))
.toList(); .toList();
@@ -266,15 +257,11 @@ class AppsProvider with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
return importedApps.length; return importedApps.length;
// } else {
// User canceled the picker
// }
} }
@override @override
void dispose() { void dispose() {
IsolateNameServer.removePortNameMapping('downloader_send_port'); foregroundSubscription.cancel();
foregroundSubscription?.cancel();
super.dispose(); super.dispose();
} }
} }

View File

@@ -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:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:obtainium/services/source_service.dart'; import 'package:obtainium/providers/source_provider.dart';
class ObtainiumNotification { class ObtainiumNotification {
late int id; late int id;

View File

@@ -1,4 +1,8 @@
// Exposes functions used to save/load app settings
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
enum ThemeSettings { system, light, dark } enum ThemeSettings { system, light, dark }
@@ -22,7 +26,6 @@ class SettingsProvider with ChangeNotifier {
} }
set theme(ThemeSettings t) { set theme(ThemeSettings t) {
print(t);
prefs?.setInt('theme', t.index); prefs?.setInt('theme', t.index);
notifyListeners(); notifyListeners();
} }
@@ -46,11 +49,24 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
checkAndFlipFirstRun() { bool checkAndFlipFirstRun() {
bool result = prefs?.getBool('firstRun') ?? true; bool result = prefs?.getBool('firstRun') ?? true;
if (result) { if (result) {
prefs?.setBool('firstRun', false); prefs?.setBool('firstRun', false);
} }
return result; return result;
} }
Future<void> 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;
}
}
}
} }

View File

@@ -1,5 +1,5 @@
// Exposes functions related to interacting with App sources and retrieving App info // Defines App sources and provides functions used to interact with them
// Stateless - not a provider // AppSource is an abstract class with a concrete implementation for each source
import 'dart:convert'; import 'dart:convert';
@@ -7,8 +7,6 @@ import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
// Sub-classes used in App Source
class AppNames { class AppNames {
late String author; late String author;
late String name; late String name;
@@ -23,34 +21,6 @@ class APKDetails {
APKDetails(this.version, this.apkUrls); 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<APKDetails> getLatestAPKDetails(String standardUrl);
AppNames getAppNames(String standardUrl);
}
escapeRegEx(String s) {
return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
return "\\${x[0]}";
});
}
List<String> 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 { class App {
late String id; late String id;
late String url; 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<String> 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<APKDetails> getLatestAPKDetails(String standardUrl);
AppNames getAppNames(String standardUrl);
}
class GitHub implements AppSource { class GitHub implements AppSource {
@override @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 // Add more source classes here so they are available via the service
AppSource getSource(String url) { AppSource getSource(String url) {
if (url.toLowerCase().contains('://github.com')) { if (url.toLowerCase().contains('://github.com')) {