diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5014f48..ccee49f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,15 +30,6 @@
-
-
-
diff --git a/lib/pages/app.dart b/lib/pages/app.dart
index 0586257..795b34a 100644
--- a/lib/pages/app.dart
+++ b/lib/pages/app.dart
@@ -17,16 +17,16 @@ class _AppPageState extends State {
@override
Widget build(BuildContext context) {
var appsProvider = context.watch();
- App? app = appsProvider.apps[widget.appId];
- if (app?.installedVersion != null) {
- appsProvider.getUpdate(app!.id);
+ AppInMemory? app = appsProvider.apps[widget.appId];
+ if (app?.app.installedVersion != null) {
+ appsProvider.getUpdate(app!.app.id);
}
return Scaffold(
appBar: AppBar(
- title: Text('${app?.author}/${app?.name}'),
+ title: Text('${app!.app.author}/${app.app.name}'),
),
body: WebView(
- initialUrl: app?.url,
+ initialUrl: app.app.url,
),
bottomSheet: Column(
mainAxisSize: MainAxisSize.min,
@@ -39,21 +39,21 @@ class _AppPageState extends State {
children: [
Expanded(
child: ElevatedButton(
- onPressed: (app?.installedVersion == null ||
- appsProvider
- .checkAppObjectForUpdate(app!)) &&
- app?.currentDownloadId == null
+ onPressed: (app.app.installedVersion == null ||
+ appsProvider.checkAppObjectForUpdate(
+ app.app)) &&
+ app.downloadProgress == null
? () {
- appsProvider
- .backgroundDownloadAndInstallApp(app!);
+ appsProvider.downloadAndInstallLatestApp(
+ app.app.id);
}
: null,
- child: Text(app?.installedVersion == null
+ child: Text(app.app.installedVersion == null
? 'Install'
: 'Update'))),
const SizedBox(width: 16.0),
ElevatedButton(
- onPressed: app?.currentDownloadId != null
+ onPressed: app.downloadProgress != null
? null
: () {
showDialog(
@@ -62,12 +62,12 @@ class _AppPageState extends State {
return AlertDialog(
title: const Text('Remove App?'),
content: Text(
- 'This will remove \'${app?.name}\' from Obtainium.${app?.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
+ 'This will remove \'${app.app.name}\' from Obtainium.${app.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
actions: [
TextButton(
onPressed: () {
appsProvider
- .removeApp(app!.id)
+ .removeApp(app.app.id)
.then((_) {
int count = 0;
Navigator.of(context).popUntil(
@@ -90,7 +90,8 @@ class _AppPageState extends State {
child: const Text('Remove'),
),
])),
- if (app?.currentDownloadId != null) const LinearProgressIndicator()
+ if (app.downloadProgress != null)
+ LinearProgressIndicator(value: app.downloadProgress)
],
),
);
diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart
index af6eee1..2884ed6 100644
--- a/lib/pages/apps.dart
+++ b/lib/pages/apps.dart
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
-import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/app.dart';
import 'package:obtainium/services/apps_provider.dart';
import 'package:provider/provider.dart';
@@ -31,18 +30,23 @@ class _AppsPageState extends State {
children: appsProvider.apps.values
.map(
(e) => ListTile(
- title: Text('${e.author}/${e.name}'),
+ title: Text('${e.app.author}/${e.app.name}'),
subtitle:
- Text(e.installedVersion ?? 'Not Installed'),
- trailing: e.installedVersion != null &&
- e.installedVersion != e.latestVersion
- ? const Text('Update Available')
- : null,
+ Text(e.app.installedVersion ?? 'Not Installed'),
+ trailing: e.downloadProgress != null
+ ? Text(
+ 'Downloading - ${e.downloadProgress!.toInt()}%')
+ : (e.app.installedVersion != null &&
+ e.app.installedVersion !=
+ e.app.latestVersion
+ ? const Text('Update Available')
+ : null),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
- builder: (context) => AppPage(appId: e.id)),
+ builder: (context) =>
+ AppPage(appId: e.app.id)),
);
},
),
diff --git a/lib/services/apps_provider.dart b/lib/services/apps_provider.dart
index c8359f3..02c3eeb 100644
--- a/lib/services/apps_provider.dart
+++ b/lib/services/apps_provider.dart
@@ -3,64 +3,45 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
-import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
-import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:obtainium/services/source_service.dart';
+import 'package:http/http.dart';
+import 'package:install_plugin_v2/install_plugin_v2.dart';
+
+class AppInMemory {
+ late App app;
+ double? downloadProgress;
+
+ AppInMemory(this.app, this.downloadProgress);
+}
class AppsProvider with ChangeNotifier {
// In memory App state (should always be kept in sync with local storage versions)
- Map apps = {};
+ Map apps = {};
bool loadingApps = false;
bool gettingUpdates = false;
- AppsProvider({bool bg = false}) {
- initializeNotifs();
- loadApps().then((_) {
- clearDownloadStates();
- });
- if (!bg) {
- initializeDownloader();
- }
- }
-
// Notifications plugin for downloads
FlutterLocalNotificationsPlugin downloaderNotifications =
FlutterLocalNotificationsPlugin();
- // Port for FlutterDownloader background/foreground communication
- final ReceivePort _port = ReceivePort();
-
// Variables to keep track of the app foreground status (installs can't run in the background)
bool isForeground = true;
StreamSubscription? foregroundSubscription;
- // Setup the FlutterDownloader plugin (call only once)
- Future initializeDownloader() async {
- // Make sure FlutterDownloader can be used
- await FlutterDownloader.initialize();
- // Set up the status update callback for FlutterDownloader
- FlutterDownloader.registerCallback(downloadCallbackBackground);
- // The actual callback is in the background isolate
- // So setup a port to pass the data to a foreground callback
- IsolateNameServer.registerPortWithName(
- _port.sendPort, 'downloader_send_port');
- _port.listen((dynamic data) {
- String id = data[0];
- DownloadTaskStatus status = data[1];
- int progress = data[2];
- downloadCallbackForeground(id, status, progress);
- });
+ AppsProvider({bool bg = false}) {
+ initializeNotifs();
// Subscribe to changes in the app foreground status
foregroundSubscription = FGBGEvents.stream.listen((event) async {
isForeground = event == FGBGType.foreground;
if (isForeground) await loadApps();
});
+ loadApps();
}
Future initializeNotifs() async {
@@ -69,15 +50,6 @@ class AppsProvider with ChangeNotifier {
android: AndroidInitializationSettings('ic_notification')));
}
- // Callback that receives FlutterDownloader status and forwards to a foreground function
- @pragma('vm:entry-point')
- static void downloadCallbackBackground(
- String id, DownloadTaskStatus status, int progress) {
- final SendPort? send =
- IsolateNameServer.lookupPortByName('downloader_send_port');
- send!.send([id, status, progress]);
- }
-
Future notify(int id, String title, String message, String channelCode,
String channelName, String channelDescription) {
return downloaderNotifications.show(
@@ -92,63 +64,44 @@ class AppsProvider with ChangeNotifier {
groupKey: 'dev.imranr.obtainium.$channelCode')));
}
- // Foreground function to act on FlutterDownloader status updates (install downloaded APK)
- void downloadCallbackForeground(
- String id, DownloadTaskStatus status, int progress) async {
- if (status == DownloadTaskStatus.complete) {
- // Wait for app to come to the foreground if not already, and notify the user
- while (!isForeground) {
- await notify(
- 1,
- 'Complete App Installation',
- 'Obtainium must be open to install Apps',
- 'COMPLETE_INSTALL',
- 'Complete App Installation',
- 'Asks the user to return to Obtanium to finish installing an App');
- if (await FGBGEvents.stream.first == FGBGType.foreground) {
- break;
- }
- }
- // Install the App (and remove warning notification if any)
- FlutterDownloader.open(taskId: id);
- downloaderNotifications.cancel(1);
- }
- // Change App status based on result (we assume user accepts install - no way to tell programatically)
- if (status == DownloadTaskStatus.complete ||
- status == DownloadTaskStatus.failed ||
- status == DownloadTaskStatus.canceled) {
- App? foundApp;
- apps.forEach((appId, app) {
- if (app.currentDownloadId == id) {
- foundApp = apps[appId];
- }
- });
- foundApp!.currentDownloadId = null;
- if (status == DownloadTaskStatus.complete) {
- foundApp!.installedVersion = foundApp!.latestVersion;
- }
- saveApp(foundApp!);
- }
- }
-
// Given a App (assumed valid), initiate an APK download (will trigger install callback when complete)
- 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(recursive: true);
- String? downloadId = await FlutterDownloader.enqueue(
- url: app.apkUrl,
- savedDir: apkDir.path,
- showNotification: true,
- openFileFromNotification: false,
- );
- if (downloadId != null) {
- app.currentDownloadId = downloadId;
- saveApp(app);
- } else {
- throw "Could not start download";
+ Future downloadAndInstallLatestApp(String appId) async {
+ if (apps[appId] == null) {
+ throw 'App not found';
}
+ StreamedResponse response =
+ await Client().send(Request('GET', Uri.parse(apps[appId]!.app.apkUrl)));
+ File downloadFile =
+ File('${(await getTemporaryDirectory()).path}/$appId.apk');
+ var length = response.contentLength;
+ var received = 0;
+ var sink = downloadFile.openWrite();
+
+ await response.stream.map((s) {
+ received += s.length;
+ apps[appId]!.downloadProgress =
+ (length != null ? received / length * 100 : 30);
+ notifyListeners();
+ return s;
+ }).pipe(sink);
+
+ await sink.close();
+ apps[appId]!.downloadProgress = null;
+ notifyListeners();
+
+ if (response.statusCode != 200) {
+ downloadFile.deleteSync();
+ throw response.reasonPhrase ?? 'Unknown Error';
+ }
+
+ var res = await InstallPlugin.installApk(
+ downloadFile.path, 'dev.imranr.obtainium');
+ print(res);
+
+ apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion;
+ saveApp(apps[appId]!.app);
+
+ downloadFile.deleteSync();
}
Future getAppsDir() async {
@@ -171,7 +124,7 @@ class AppsProvider with ChangeNotifier {
for (int i = 0; i < appFiles.length; i++) {
App app =
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
- apps.putIfAbsent(app.id, () => app);
+ apps.putIfAbsent(app.id, () => AppInMemory(app, null));
}
loadingApps = false;
notifyListeners();
@@ -180,25 +133,11 @@ class AppsProvider with ChangeNotifier {
Future saveApp(App app) async {
File('${(await getAppsDir()).path}/${app.id}.json')
.writeAsStringSync(jsonEncode(app.toJson()));
- apps.update(app.id, (value) => app, ifAbsent: () => app);
+ apps.update(app.id, (value) => AppInMemory(app, value.downloadProgress),
+ ifAbsent: () => AppInMemory(app, null));
notifyListeners();
}
- Future clearDownloadStates() async {
- var appList = apps.values.toList();
- int count = 0;
- for (int i = 0; i < appList.length; i++) {
- if (appList[i].currentDownloadId != null) {
- apps[appList[i].id]?.currentDownloadId = null;
- await saveApp(apps[appList[i].id]!);
- count++;
- }
- }
- if (count > 0) {
- notifyListeners();
- }
- }
-
Future removeApp(String appId) async {
File file = File('${(await getAppsDir()).path}/$appId.json');
if (file.existsSync()) {
@@ -214,11 +153,11 @@ class AppsProvider with ChangeNotifier {
if (!apps.containsKey(app.id)) {
throw 'App not found';
}
- return app.latestVersion != apps[app.id]?.installedVersion;
+ return app.latestVersion != apps[app.id]?.app.installedVersion;
}
Future getUpdate(String appId) async {
- App? currentApp = apps[appId];
+ App? currentApp = apps[appId]!.app;
App newApp = await SourceService().getApp(currentApp!.url);
if (newApp.latestVersion != currentApp.latestVersion) {
newApp.installedVersion = currentApp.installedVersion;
@@ -248,9 +187,9 @@ class AppsProvider with ChangeNotifier {
Future installUpdates() async {
List appIds = apps.keys.toList();
for (int i = 0; i < appIds.length; i++) {
- App? app = apps[appIds[i]];
+ App? app = apps[appIds[i]]!.app;
if (app!.installedVersion != app.latestVersion) {
- await backgroundDownloadAndInstallApp(app);
+ await downloadAndInstallLatestApp(app.id);
}
}
}
diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart
index f32336c..7c1dce2 100644
--- a/lib/services/source_service.dart
+++ b/lib/services/source_service.dart
@@ -3,6 +3,7 @@
import 'dart:convert';
import 'package:http/http.dart';
+import 'package:html/parser.dart';
// Sub-classes used in App Source
@@ -28,6 +29,12 @@ abstract class AppSource {
AppNames getAppNames(String standardUrl);
}
+escapeRegEx(String s) {
+ return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
+ return "\\${x[0]}";
+ });
+}
+
// App class
class App {
@@ -38,9 +45,8 @@ class App {
String? installedVersion;
late String latestVersion;
late String apkUrl;
- String? currentDownloadId;
App(this.id, this.url, this.author, this.name, this.installedVersion,
- this.latestVersion, this.apkUrl, this.currentDownloadId);
+ this.latestVersion, this.apkUrl);
@override
String toString() {
@@ -56,10 +62,7 @@ class App {
? null
: json['installedVersion'] as String,
json['latestVersion'] as String,
- json['apkUrl'] as String,
- json['currentDownloadId'] == null
- ? null
- : json['currentDownloadId'] as String);
+ json['apkUrl'] as String);
Map toJson() => {
'id': id,
@@ -69,7 +72,6 @@ class App {
'installedVersion': installedVersion,
'latestVersion': latestVersion,
'apkUrl': apkUrl,
- 'currentDownloadId': currentDownloadId
};
}
@@ -94,20 +96,22 @@ class GitHub implements AppSource {
@override
Future getLatestAPKUrl(String standardUrl) async {
- Response res = await get(Uri.parse(
- '${convertURL(standardUrl, 'api.github.com/repos')}/releases/latest'));
+ Response res = await get(Uri.parse('$standardUrl/releases/latest'));
if (res.statusCode == 200) {
- dynamic release = jsonDecode(res.body);
- for (int i = 0; i < release['assets'].length; i++) {
- if (release['assets'][i]['name']
- .toString()
- .toLowerCase()
- .endsWith('.apk')) {
- return APKDetails(release['tag_name'],
- release['assets'][i]['browser_download_url']);
- }
+ var standardUri = Uri.parse(standardUrl);
+ var parsedHtml = parse(res.body);
+ var apkUrlList = parsedHtml.querySelectorAll('a').where((element) {
+ return RegExp(
+ '^${escapeRegEx(standardUri.path)}/releases/download/*/(?!/).*.apk\$',
+ caseSensitive: false)
+ .hasMatch(element.attributes['href']!);
+ }).toList();
+ String? version = parsedHtml.querySelector('h1')?.innerHtml;
+ if (apkUrlList.isEmpty || version == null) {
+ throw 'No APK found';
}
- throw 'No APK found';
+ return APKDetails(
+ version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}');
} else {
throw 'Unable to fetch release info';
}
@@ -147,7 +151,6 @@ class SourceService {
names.name[0].toUpperCase() + names.name.substring(1),
null,
apk.version,
- apk.downloadUrl,
- null);
+ apk.downloadUrl);
}
}
diff --git a/pubspec.lock b/pubspec.lock
index 5faf770..83ed880 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -71,6 +71,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.17.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -118,13 +125,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_downloader:
- dependency: "direct main"
- description:
- name: flutter_downloader
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.8.1"
flutter_fgbg:
dependency: "direct main"
description:
@@ -172,11 +172,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_web_plugins:
- dependency: transitive
- description: flutter
- source: sdk
- version: "0.0.0"
+ html:
+ dependency: "direct main"
+ description:
+ name: html
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.15.0"
http:
dependency: "direct main"
description:
@@ -198,13 +200,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
- js:
- dependency: transitive
+ install_plugin_v2:
+ dependency: "direct main"
description:
- name: js
+ name: install_plugin_v2
url: "https://pub.dartlang.org"
source: hosted
- version: "0.6.4"
+ version: "1.0.0"
json_annotation:
dependency: transitive
description:
@@ -392,13 +394,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.0"
- toast:
- dependency: "direct main"
- description:
- name: toast
- url: "https://pub.dartlang.org"
- source: hosted
- version: "0.3.0"
typed_data:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 06f9557..3b283f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -37,15 +37,15 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
path_provider: ^2.0.11
- flutter_downloader: ^1.8.1
flutter_fgbg: ^0.2.0
flutter_local_notifications: ^9.7.0
provider: ^6.0.3
http: ^0.13.5
- toast: ^0.3.0
webview_flutter: ^3.0.4
workmanager: ^0.5.0
dynamic_color: ^1.5.3
+ install_plugin_v2: ^1.0.0
+ html: ^0.15.0
dev_dependencies: