From bbc453ef645e2bfbcf7c611f7b4bebc3819d7ad2 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Thu, 11 Aug 2022 16:18:00 -0400 Subject: [PATCH] APK Service is ready --- .gitignore | 3 + README.md | 19 ++- android/app/proguard-rules.pro | 32 +++++ .../app/src/main/res/drawable/ic_launcher.png | Bin 0 -> 544 bytes lib/main.dart | 91 +++----------- lib/services/apk_service.dart | 112 ++++++++++++++++++ pubspec.lock | 77 ++++++++++++ pubspec.yaml | 3 + 8 files changed, 249 insertions(+), 88 deletions(-) create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/res/drawable/ic_launcher.png create mode 100644 lib/services/apk_service.dart diff --git a/.gitignore b/.gitignore index 24476c5..6d8b901 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Custom +TODO.txt \ No newline at end of file diff --git a/README.md b/README.md index cf0f1f7..2487b06 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,11 @@ -# obtainium +# Obtainium -A new Flutter project. +Get Android App Updates Directly From the Source. -## Getting Started +Obtainium allows you to install and update Open-Source Apps directly from their GitHub or GitLab releases. -This project is a starting point for a Flutter application. +***Work In Progress - Currently Unusable*** -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +## Limitations +- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected. +- Apps that are already installed are not indicated as such, since GitHub and GitLab do not provide App IDs (like `org.example.app`) to allow for comparisons. \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..ea6dd79 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,32 @@ +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +##---------------End: proguard configuration for Gson ---------- \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher.png b/android/app/src/main/res/drawable/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/lib/main.dart b/lib/main.dart index 793c848..7565aae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,72 +1,19 @@ -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math'; -import 'dart:ui'; - import 'package:flutter/material.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:install_plugin_v2/install_plugin_v2.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; -import 'package:app_installer/app_installer.dart'; - -// Port for FlutterDownloader background/foreground communication -ReceivePort _port = ReceivePort(); +import 'package:obtainium/services/apk_service.dart'; +import 'package:provider/provider.dart'; void main() async { - await initializeDownloader(); - runApp(const MyApp()); -} - -// Setup the FlutterDownloader plugin -Future initializeDownloader() async { - // Make sure FlutterDownloader can be used + ; WidgetsFlutterBinding.ensureInitialized(); - 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); - }); -} - -// Callback that receives FlutterDownloader status and forwards to a foreground function -@pragma('vm:entry-point') -void downloadCallbackBackground( - String id, DownloadTaskStatus status, int progress) { - final SendPort? send = - IsolateNameServer.lookupPortByName('downloader_send_port'); - send!.send([id, status, progress]); -} - -// Foreground function to act on FlutterDownloader status updates (install then delete downloaded APK) -void downloadCallbackForeground( - String id, DownloadTaskStatus status, int progress) async { - if (status == DownloadTaskStatus.complete) { - FlutterDownloader.open(taskId: id); - } -} - -// Given a URL (assumed valid), initiate an APK download (will trigger install callback when complete) -void downloadAPK(String url, String appId) async { - var apkDir = Directory( - "${(await getExternalStorageDirectory())?.path as String}/$appId"); - if (apkDir.existsSync()) apkDir.deleteSync(recursive: true); - apkDir.createSync(); - await FlutterDownloader.enqueue( - url: url, - savedDir: apkDir.path, - showNotification: true, - openFileFromNotification: true, - ); + runApp(MultiProvider( + providers: [ + Provider( + create: (context) => APKService(), + dispose: (context, apkInstallService) => apkInstallService.dispose(), + ), + ], + child: const MyApp(), + )); } // Extract a GitHub project name and author account name from a GitHub URL (can be any sub-URL of the project) @@ -83,14 +30,6 @@ Map? getAppNamesFromGitHubURL(String url) { return null; } -// Future getAPKDir() async { -// var apkDir = Directory("${(await getExternalStorageDirectory())!.path}/apks"); -// if (!apkDir.existsSync()) { -// apkDir.createSync(); -// } -// return apkDir; -// } - class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -144,7 +83,9 @@ class _MyHomePageState extends State { onPressed: () { var names = getAppNamesFromGitHubURL(urls[ind]); if (names != null) { - downloadAPK(urls[ind], "${names["author"]!}_${names["appName"]!}"); + Provider.of(context, listen: false) + .downloadAndInstallAPK( + urls[ind], "${names["author"]!}_${names["appName"]!}"); setState(() { ind = ind == (urls.length - 1) ? 0 : ind + 1; }); @@ -158,8 +99,6 @@ class _MyHomePageState extends State { @override void dispose() { - // Remove the FlutterDownloader communication port - IsolateNameServer.removePortNameMapping('downloader_send_port'); super.dispose(); } } diff --git a/lib/services/apk_service.dart b/lib/services/apk_service.dart new file mode 100644 index 0000000..b8c1c00 --- /dev/null +++ b/lib/services/apk_service.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:install_plugin_v2/install_plugin_v2.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:app_installer/app_installer.dart'; +import 'package:flutter_fgbg/flutter_fgbg.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class APKService { + APKService() { + initializeDownloader(); + } + + // Notifications plugin for downloads + FlutterLocalNotificationsPlugin downloaderNotifications = + FlutterLocalNotificationsPlugin(); + + // Port for FlutterDownloader background/foreground communication + 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 in main()) + 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); + }); + // Initialize the notifications service + await downloaderNotifications.initialize(const InitializationSettings( + android: AndroidInitializationSettings('ic_launcher'))); + // Subscribe to changes in the app foreground status + foregroundSubscription = FGBGEvents.stream.listen((event) async { + isForeground = event == FGBGType.foreground; + }); + } + + // Clean up after initializeDownloader() (call in dispose()) + void dispose() { + IsolateNameServer.removePortNameMapping('downloader_send_port'); + foregroundSubscription?.cancel(); + } + + // 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]); + } + + // 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 downloaderNotifications.show( + 1, + 'Complete App Installation', + 'Obtainium must be open to install Apps', + const NotificationDetails( + android: AndroidNotificationDetails( + 'COMPLETE_INSTALL', 'Complete App Installation', + channelDescription: + 'Ask the user to return to Obtanium to finish installing an App', + importance: Importance.max, + priority: Priority.max, + groupKey: 'dev.imranr.obtainium.COMPLETE_INSTALL'))); + if (await FGBGEvents.stream.first == FGBGType.foreground) { + break; + } + } + FlutterDownloader.open(taskId: id); + downloaderNotifications.cancel(1); + } + } + + // Given a URL (assumed valid), initiate an APK download (will trigger install callback when complete) + void downloadAndInstallAPK(String url, String appId) async { + var apkDir = Directory( + "${(await getExternalStorageDirectory())?.path as String}/$appId"); + if (apkDir.existsSync()) apkDir.deleteSync(recursive: true); + apkDir.createSync(); + await FlutterDownloader.enqueue( + url: url, + savedDir: apkDir.path, + showNotification: true, + openFileFromNotification: false, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 576697d..9035a8a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" async: dependency: transitive description: @@ -50,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.7" fake_async: dependency: transitive description: @@ -90,6 +104,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + flutter_fgbg: + dependency: "direct main" + description: + name: flutter_fgbg + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" flutter_lints: dependency: "direct dev" description: @@ -97,6 +118,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "9.7.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -156,6 +198,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -247,6 +296,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" platform: dependency: transitive description: @@ -268,6 +324,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" sky_engine: dependency: transitive description: flutter @@ -315,6 +378,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.12" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" vector_math: dependency: transitive description: @@ -336,6 +406,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" sdks: dart: ">=2.19.0-79.0.dev <3.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8f54416..3ef3c6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,9 @@ dependencies: path_provider: ^2.0.11 flutter_downloader: ^1.8.1 app_installer: ^1.1.0 + flutter_fgbg: ^0.2.0 + flutter_local_notifications: ^9.7.0 + provider: ^6.0.3 dev_dependencies: flutter_test: