diff --git a/README.md b/README.md index c28fbbc9..b05223a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![](./android/app/src/main/res/drawable/ic_notification.png) Obtainium +# ![Obtainium Icon](./android/app/src/main/res/drawable/ic_notification.png) Obtainium Get Android App Updates Directly From the Source. @@ -13,6 +13,7 @@ Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to ## Limitations - App installs are assumed to have succeeded; failures and cancelled installs cannot be detected. - Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin. +- For GitHub, data is gathered using Web scraping and can easily break due to changes in website design. More reliable methods are either insufficient (GitHub RSS) or subject to rate limits (GitHub API). This may also apply to new sources added in the future. ## Screenshots diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 203c0b29..33dac646 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -46,14 +46,14 @@ class _AppPageState extends State { appsProvider .checkAppObjectForUpdate( app!.app)) && - app?.downloadProgress == null + !appsProvider.areDownloadsRunning() ? () { HapticFeedback.heavyImpact(); appsProvider .downloadAndInstallLatestApp( [app!.app.id], context).then((res) { - if (res) { + if (res && mounted) { Navigator.of(context).pop(); } }); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 14bbca82..d93a47b3 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -22,9 +22,7 @@ class _AppsPageState extends State { floatingActionButton: existingUpdateAppIds.isEmpty ? null : ElevatedButton.icon( - onPressed: appsProvider.apps.values - .where((element) => element.downloadProgress != null) - .isNotEmpty + onPressed: appsProvider.areDownloadsRunning() ? null : () { HapticFeedback.heavyImpact(); @@ -60,7 +58,7 @@ class _AppsPageState extends State { e.app.installedVersion ?? 'Not Installed'), trailing: e.downloadProgress != null ? Text( - 'Downloading - ${e.downloadProgress!.toInt()}%') + 'Downloading - ${e.downloadProgress?.toInt()}%') : (e.app.installedVersion != null && e.app.installedVersion != e.app.latestVersion diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index d02ddc35..209bd626 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -80,6 +80,10 @@ class AppsProvider with ChangeNotifier { return ApkFile(appId, downloadFile); } + bool areDownloadsRunning() => apps.values + .where((element) => element.downloadProgress != null) + .isNotEmpty; + // 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 @@ -112,6 +116,7 @@ class AppsProvider with ChangeNotifier { await notificationsProvider.notify(completeInstallationNotification, cancelExisting: true); await FGBGEvents.stream.first == FGBGType.foreground; + await notificationsProvider.cancel(completeInstallationNotification.id); // 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 diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 4b52d49e..cff8809a 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -165,7 +165,7 @@ class GitLab implements AppSource { var parsedHtml = parse(res.body); var entry = parsedHtml.querySelector('entry'); var entryContent = - parse(parseFragment(entry!.querySelector('content')!.innerHtml).text); + parse(parseFragment(entry?.querySelector('content')!.innerHtml).text); var apkUrlList = getLinksFromParsedHTML( entryContent, RegExp( @@ -176,7 +176,7 @@ class GitLab implements AppSource { throw 'No APK found'; } - var entryId = entry.querySelector('id')?.innerHtml; + var entryId = entry?.querySelector('id')?.innerHtml; var version = entryId == null ? null : Uri.parse(entryId).pathSegments.last; if (version == null) { @@ -195,6 +195,39 @@ class GitLab implements AppSource { } } +class Signal implements AppSource { + @override + String sourceId = 'signal'; + + @override + String standardizeURL(String url) { + return 'https://signal.org'; + } + + @override + Future getLatestAPKDetails(String standardUrl) async { + Response res = + await get(Uri.parse('https://updates.signal.org/android/latest.json')); + if (res.statusCode == 200) { + var json = jsonDecode(res.body); + String? apkUrl = json['url']; + if (apkUrl == null) { + throw 'No APK found'; + } + String? version = json['versionName']; + if (version == null) { + throw 'Could not determine latest release version'; + } + return APKDetails(version, [apkUrl]); + } else { + throw 'Unable to fetch release info'; + } + } + + @override + AppNames getAppNames(String standardUrl) => AppNames('signal', 'signal'); +} + class SourceProvider { // Add more source classes here so they are available via the service AppSource getSource(String url) { @@ -202,6 +235,8 @@ class SourceProvider { return GitHub(); } else if (url.toLowerCase().contains('://gitlab.com')) { return GitLab(); + } else if (url.toLowerCase().contains('://signal.org')) { + return Signal(); } throw 'URL does not match a known source'; } @@ -228,5 +263,5 @@ class SourceProvider { apk.apkUrls); } - List getSourceHosts() => ['github.com', 'gitlab.com']; + List getSourceHosts() => ['github.com', 'gitlab.com', 'signal.org']; }