diff --git a/lib/main.dart b/lib/main.dart index bfccf32..5ee695b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,12 +14,12 @@ void main() async { // Extract a GitHub project name and author account name from a GitHub URL (can be any sub-URL of the project) Map? getAppNamesFromGitHubURL(String url) { RegExp regex = RegExp(r'://github.com/[^/]*/[^/]*'); - var match = regex.firstMatch(url.toLowerCase()); + RegExpMatch? match = regex.firstMatch(url.toLowerCase()); if (match != null) { - var uri = url.substring(match.start + 14, match.end); - var slashIndex = uri.indexOf('/'); - var author = uri.substring(0, slashIndex); - var appName = uri.substring(slashIndex + 1); + String uri = url.substring(match.start + 14, match.end); + int slashIndex = uri.indexOf('/'); + String author = uri.substring(0, slashIndex); + String appName = uri.substring(slashIndex + 1); return {'author': author, 'appName': appName}; } return null; @@ -51,7 +51,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { int ind = 0; - var urls = [ + List urls = [ 'https://github.com/Ashinch/ReadYou/releases/download', // Should work 'http://github.com/syncthing/syncthing-android/releases/tag/1.20.4', // Should work 'https://github.com/videolan/vlc' // Should not diff --git a/lib/services/apps_provider.dart b/lib/services/apps_provider.dart index 7aff147..6058d1f 100644 --- a/lib/services/apps_provider.dart +++ b/lib/services/apps_provider.dart @@ -1,6 +1,7 @@ // Provider that manages App-related state and provides functions to retrieve App info download/install Apps import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; @@ -15,6 +16,7 @@ import 'package:obtainium/services/source_service.dart'; class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; + bool loadingApps = false; AppsProvider() { initializeDownloader(); @@ -88,70 +90,107 @@ class AppsProvider with ChangeNotifier { break; } } + // Change App status to no longer downloading + App? foundApp; + apps.forEach((id, app) { + if (app.currentDownloadId == id) { + foundApp = apps[app.id]; + } + }); + foundApp!.currentDownloadId = null; + saveApp(foundApp!); + // Install the App (and remove warning notification if any) FlutterDownloader.open(taskId: id); downloaderNotifications.cancel(1); } } // Given a URL (assumed valid), initiate an APK download (will trigger install callback when complete) - Future backgroundDownloadAndInstallAPK(String url, String appId) async { - var apkDir = Directory( - '${(await getExternalStorageDirectory())?.path as String}/$appId'); + Future backgroundDownloadAndInstallApp(App app) async { + Directory apkDir = Directory( + '${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}'); if (apkDir.existsSync()) apkDir.deleteSync(recursive: true); - apkDir.createSync(); - await FlutterDownloader.enqueue( - url: url, + apkDir.createSync(recursive: true); + String? downloadId = await FlutterDownloader.enqueue( + url: app.url, savedDir: apkDir.path, showNotification: true, openFileFromNotification: false, ); + if (downloadId != null) { + app.currentDownloadId = downloadId; + saveApp(app); + } else { + throw "Could not start download"; + } } - void loadApps() { - // TODO: Load Apps JSON and fill the array + Future getAppsDir() async { + Directory appsDir = Directory( + '${(await getExternalStorageDirectory())?.path as String}/apps'); + if (!appsDir.existsSync()) { + appsDir.createSync(); + } + return appsDir; + } + + Future loadApps() async { + loadingApps = true; + notifyListeners(); + List appFiles = (await getAppsDir()) + .listSync() + .where((item) => item.path.toLowerCase().endsWith('.json')) + .toList(); + apps.clear(); + for (int i = 0; i < appFiles.length; i++) { + App app = + App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync())); + apps.putIfAbsent(app.id, () => app); + } + loadingApps = false; notifyListeners(); } - void saveApp(App app) { - // TODO: Save/update an App JSON and update the array + Future saveApp(App app) async { + File('${(await getAppsDir()).path}/${app.id}.json') + .writeAsStringSync(jsonEncode(app)); + apps.update(app.id, (value) => app, ifAbsent: () => app); notifyListeners(); } bool checkUpdate(App app) { - // TODO: Check the given App against the existing version in the array (if it does not exist, throw an error) - return false; + if (!apps.containsKey(app.id)) { + throw 'App not found'; + } + return app.latestVersion != apps[app.id]?.installedVersion; } Future installApp(String url) async { - var app = await SourceService().getApp(url); - await backgroundDownloadAndInstallAPK(app.apkUrl, app.id); - // TODO: Apps array should notify consumers about download progress (will need to change FlutterDownloader callbacks) - saveApp(app); + App app = await SourceService().getApp(url); + await backgroundDownloadAndInstallApp(app); } Future> checkUpdates() async { List updates = []; - var appIds = apps.keys.toList(); - for (var i = 0; i < appIds.length; i++) { - var currentApp = apps[appIds[i]]; - var newApp = await SourceService().getApp(currentApp!.url); + List appIds = apps.keys.toList(); + for (int i = 0; i < appIds.length; i++) { + App? currentApp = apps[appIds[i]]; + App newApp = await SourceService().getApp(currentApp!.url); if (newApp.latestVersion != currentApp.latestVersion) { newApp.installedVersion = currentApp.installedVersion; updates.add(newApp); - saveApp(newApp); + await saveApp(newApp); } } return updates; } Future installUpdates() async { - var appIds = apps.keys.toList(); - for (var i = 0; i < appIds.length; i++) { - var app = apps[appIds[i]]; - if (app != null) { - if (app.installedVersion != app.latestVersion) { - await installApp(app.apkUrl); - } + List appIds = apps.keys.toList(); + for (int i = 0; i < appIds.length; i++) { + App? app = apps[appIds[i]]; + if (app!.installedVersion != app.latestVersion) { + await installApp(app.apkUrl); } } } diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart index 18d1cd8..60a1bfe 100644 --- a/lib/services/source_service.dart +++ b/lib/services/source_service.dart @@ -36,13 +36,30 @@ class App { String? installedVersion; late String latestVersion; late String apkUrl; - App(this.id, this.url, this.installedVersion, this.latestVersion, - this.apkUrl); + String? currentDownloadId; + App(this.id, this.url, this.installedVersion, this.latestVersion, this.apkUrl, + this.currentDownloadId); @override String toString() { return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; } + + factory App.fromJson(Map json) => _appFromJson(json); +} + +App _appFromJson(Map json) { + return App( + json['id'] as String, + json['url'] as String, + json['installedVersion'] == null + ? null + : json['installedVersion'] as String, + json['latestVersion'] as String, + json['apkUrl'] as String, + json['currentDownloadId'] == null + ? null + : json['currentDownloadId'] as String); } // Specific App Source classes @@ -51,7 +68,7 @@ class GitHub implements AppSource { @override String standardizeURL(String url) { RegExp standardUrlRegEx = RegExp(r'^https?://github.com/[^/]*/[^/]*'); - var match = standardUrlRegEx.firstMatch(url.toLowerCase()); + RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { throw 'Not a valid URL'; } @@ -69,8 +86,8 @@ class GitHub implements AppSource { Response res = await get(Uri.parse( '${convertURL(standardUrl, 'api.github.com/repos')}/releases/latest')); if (res.statusCode == 200) { - var release = jsonDecode(res.body); - for (var i = 0; i < release['assets'].length; i++) { + dynamic release = jsonDecode(res.body); + for (int i = 0; i < release['assets'].length; i++) { if (release['assets'][i]['name'] .toString() .toLowerCase() @@ -95,7 +112,7 @@ class GitHub implements AppSource { class SourceService { // Add more source classes here so they are available via the service - var github = GitHub(); + AppSource github = GitHub(); AppSource getSource(String url) { if (url.toLowerCase().contains('://github.com')) { return github; @@ -109,6 +126,6 @@ class SourceService { AppNames names = source.getAppNames(standardUrl); APKDetails apk = await source.getLatestAPKUrl(standardUrl); return App('${names.author}_${names.name}', standardUrl, null, apk.version, - apk.downloadUrl); + apk.downloadUrl, null); } }