diff --git a/lib/main.dart b/lib/main.dart index b7b95f3..5920c1e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:obtainium/services/apk_service.dart'; -import 'package:obtainium/services/app_service.dart'; import 'package:obtainium/services/source_service.dart'; import 'package:provider/provider.dart'; @@ -11,10 +10,7 @@ void main() async { Provider( create: (context) => APKService(), dispose: (context, apkInstallService) => apkInstallService.dispose(), - ), - Provider(create: (context) => SourceService()), - Provider( - create: (context) => AppService(Provider.of(context))) + ) ], child: const MyApp(), )); @@ -22,14 +18,14 @@ 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/[^/]*/[^/]*"); + RegExp regex = RegExp(r'://github.com/[^/]*/[^/]*'); var match = regex.firstMatch(url.toLowerCase()); if (match != null) { var uri = url.substring(match.start + 14, match.end); - var slashIndex = uri.indexOf("/"); + var slashIndex = uri.indexOf('/'); var author = uri.substring(0, slashIndex); var appName = uri.substring(slashIndex + 1); - return {"author": author, "appName": appName}; + return {'author': author, 'appName': appName}; } return null; } @@ -61,9 +57,9 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { int ind = 0; var 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 + '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 ]; @override @@ -77,7 +73,7 @@ class _MyHomePageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - urls[ind] + ind.toString(), + urls[ind], style: Theme.of(context).textTheme.headline4, ), ], @@ -85,9 +81,9 @@ class _MyHomePageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () { - Provider.of(context).getApp(urls[ind]).then((app) { + SourceService().getApp(urls[ind]).then((app) { Provider.of(context, listen: false) - .downloadAndInstallAPK(app.apkUrl, app.id); + .backgroundDownloadAndInstallAPK(app.apkUrl, app.id); setState(() { ind = ind == (urls.length - 1) ? 0 : ind + 1; }); diff --git a/lib/services/apk_service.dart b/lib/services/apk_service.dart index 5dc9d56..ed269c4 100644 --- a/lib/services/apk_service.dart +++ b/lib/services/apk_service.dart @@ -1,3 +1,6 @@ +// Exposes functions related to interacting with App sources and retrieving App info +// Stateless, but used as a Provider as it must be a singleton (must only initialize once, be in scope at all times) + import 'dart:async'; import 'dart:io'; import 'dart:isolate'; @@ -18,7 +21,7 @@ class APKService { FlutterLocalNotificationsPlugin(); // Port for FlutterDownloader background/foreground communication - ReceivePort _port = ReceivePort(); + final ReceivePort _port = ReceivePort(); // Variables to keep track of the app foreground status (installs can't run in the background) bool isForeground = true; @@ -92,9 +95,9 @@ class APKService { } // Given a URL (assumed valid), initiate an APK download (will trigger install callback when complete) - void downloadAndInstallAPK(String url, String appId) async { + Future backgroundDownloadAndInstallAPK(String url, String appId) async { var apkDir = Directory( - "${(await getExternalStorageDirectory())?.path as String}/$appId"); + '${(await getExternalStorageDirectory())?.path as String}/$appId'); if (apkDir.existsSync()) apkDir.deleteSync(recursive: true); apkDir.createSync(); await FlutterDownloader.enqueue( diff --git a/lib/services/app_service.dart b/lib/services/app_service.dart deleted file mode 100644 index 7a01004..0000000 --- a/lib/services/app_service.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:obtainium/services/source_service.dart'; - -class App { - late String id; - late String url; - String? installedVersion; - late String latestVersion; - late String apkUrl; - App(this.id, this.url, this.installedVersion, this.latestVersion, - this.apkUrl); -} - -class AppService { - late SourceService sourceService; - AppService(this.sourceService); - - Future getApp(String url) async { - AppSource source = sourceService.getSource(url); - String standardUrl = source.standardizeURL(url); - AppNames names = source.getAppNames(standardUrl); - APKDetails apk = await source.getLatestAPKUrl(standardUrl); - return App("${names.author}_${names.name}", standardUrl, null, apk.version, - apk.downloadUrl); - } - - // Load Apps, Save App -} diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart index 5f3281f..18d1cd8 100644 --- a/lib/services/source_service.dart +++ b/lib/services/source_service.dart @@ -1,3 +1,6 @@ +// Exposes functions related to interacting with App sources and retrieving App info +// Stateless - not a provider + import 'dart:convert'; import 'package:http/http.dart'; @@ -25,52 +28,67 @@ abstract class AppSource { AppNames getAppNames(String standardUrl); } +// App class + +class App { + late String id; + late String url; + String? installedVersion; + late String latestVersion; + late String apkUrl; + App(this.id, this.url, this.installedVersion, this.latestVersion, + this.apkUrl); + + @override + String toString() { + return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; + } +} + // Specific App Source classes class GitHub implements AppSource { @override String standardizeURL(String url) { - RegExp standardUrlRegEx = RegExp(r"^https?://github.com/[^/]*/[^/]*"); + RegExp standardUrlRegEx = RegExp(r'^https?://github.com/[^/]*/[^/]*'); var match = standardUrlRegEx.firstMatch(url.toLowerCase()); if (match == null) { - throw "Not a valid URL"; + throw 'Not a valid URL'; } return url.substring(0, match.end); } - String convertURLToRawContentURL(String url) { + String convertURL(String url, String replaceText) { int tempInd1 = url.indexOf('://') + 3; int tempInd2 = url.substring(tempInd1).indexOf('/') + tempInd1; - return "${url.substring(0, tempInd1)}raw.githubusercontent.com${url.substring(tempInd2)}"; + return '${url.substring(0, tempInd1)}$replaceText${url.substring(tempInd2)}'; } @override Future getLatestAPKUrl(String standardUrl) async { - int tempInd = standardUrl.indexOf('://') + 3; Response res = await get(Uri.parse( - "${standardUrl.substring(0, tempInd)}api.${standardUrl.substring(tempInd)}/releases/latest")); + '${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++) { - if (release['assets'][i] - .name + if (release['assets'][i]['name'] .toString() .toLowerCase() - .endsWith(".apk")) { + .endsWith('.apk')) { return APKDetails(release['tag_name'], release['assets'][i]['browser_download_url']); } } - throw "No APK found"; + throw 'No APK found'; } else { - throw "Unable to fetch release info"; + throw 'Unable to fetch release info'; } } @override AppNames getAppNames(String standardUrl) { String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); - List names = temp.substring(temp.indexOf('/')).split('/'); + List names = temp.substring(temp.indexOf('/') + 1).split('/'); return AppNames(names[0], names[1]); } } @@ -82,6 +100,15 @@ class SourceService { if (url.toLowerCase().contains('://github.com')) { return github; } - throw "URL does not match a known source"; + throw 'URL does not match a known source'; + } + + Future getApp(String url) async { + AppSource source = getSource(url); + String standardUrl = source.standardizeURL(url); + AppNames names = source.getAppNames(standardUrl); + APKDetails apk = await source.getLatestAPKUrl(standardUrl); + return App('${names.author}_${names.name}', standardUrl, null, apk.version, + apk.downloadUrl); } }