diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 08a3cb1..0e28b87 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -49,7 +49,7 @@ class _AppPageState extends State { ? () { appsProvider .downloadAndInstallLatestApp( - app!.app.id, context); + [app!.app.id], context); } : null, child: Text(app?.app.installedVersion == null diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index bf5253e..a2bbc13 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -30,10 +30,8 @@ class _AppsPageState extends State { .read() .getInstallPermission() .then((_) { - for (var e in existingUpdateAppIds) { - appsProvider.downloadAndInstallLatestApp( - e, context); - } + appsProvider.downloadAndInstallLatestApp( + existingUpdateAppIds, context); }); }, icon: const Icon(Icons.update), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b70af35..e98a43e 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -21,6 +21,12 @@ class AppInMemory { AppInMemory(this.app, this.downloadProgress); } +class ApkFile { + String appId; + File file; + ApkFile(this.appId, this.file); +} + class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; @@ -42,45 +48,7 @@ class AppsProvider with ChangeNotifier { loadApps(); } - // 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(); - if (apps[appId] == null) { - throw 'App not found'; - } - String apkUrl = apps[appId]!.app.apkUrls.last; - if (apps[appId]!.app.apkUrls.length > 1) { - await showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - title: const Text('Pick an APK'), - content: Column(children: [ - Text( - '${apps[appId]!.app.name} has more than one package - pick one.'), - ...apps[appId]!.app.apkUrls.map((u) => ListTile( - title: Text(Uri.parse(u).pathSegments.last), - leading: Radio( - value: u, - groupValue: apkUrl, - onChanged: (String? val) { - apkUrl = val!; - }))) - ]), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Continue')) - ], - ); - }); - } + Future downloadApp(String apkUrl, String appId) async { StreamedResponse response = await Client().send(Request('GET', Uri.parse(apkUrl))); File downloadFile = @@ -108,26 +76,74 @@ class AppsProvider with ChangeNotifier { downloadFile.deleteSync(); throw response.reasonPhrase ?? 'Unknown Error'; } + return ApkFile(appId, downloadFile); + } - while (!isForeground) { + // 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( + List appIds, BuildContext context) async { + NotificationsProvider notificationsProvider = + context.read(); + Map appsToInstall = {}; + for (var id in appIds) { + if (apps[id] == null) { + throw 'App not found'; + } + String apkUrl = apps[id]!.app.apkUrls.last; + if (apps[id]!.app.apkUrls.length > 1) { + await showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + title: const Text('Pick an APK'), + content: Column(children: [ + Text( + '${apps[id]!.app.name} has more than one package - pick one.'), + ...apps[id]!.app.apkUrls.map((u) => ListTile( + title: Text(Uri.parse(u).pathSegments.last), + leading: Radio( + value: u, + groupValue: apkUrl, + onChanged: (String? val) { + apkUrl = val!; + }))) + ]), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Continue')) + ], + ); + }); + } + appsToInstall.putIfAbsent(id, () => apkUrl); + } + + List downloadedFiles = await Future.wait(appsToInstall.entries + .map((entry) => downloadApp(entry.value, entry.key))); + + if (!isForeground) { await notificationsProvider.notify(completeInstallationNotification, cancelExisting: true); - 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 - } + await FGBGEvents.stream.first == FGBGType.foreground; + // 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 } // 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 - await InstallPlugin.installApk(downloadFile.path, 'dev.imranr.obtainium'); - - apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion; - await saveApp(apps[appId]!.app); + for (var f in downloadedFiles) { + await InstallPlugin.installApk(f.file.path, 'dev.imranr.obtainium'); + apps[f.appId]!.app.installedVersion = apps[f.appId]!.app.latestVersion; + await saveApp(apps[f.appId]!.app); + } } Future getAppsDir() async {