APK Service is ready

This commit is contained in:
Imran Remtulla
2022-08-11 16:18:00 -04:00
parent 565a6434c2
commit bbc453ef64
8 changed files with 249 additions and 88 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Custom
TODO.txt

View File

@ -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: ## Limitations
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) - 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.
- [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.

32
android/app/proguard-rules.pro vendored Normal file
View File

@ -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.** { <fields>; }
# 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 <fields>;
}
# 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 ----------

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

View File

@ -1,72 +1,19 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; import 'package:obtainium/services/apk_service.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart'; import 'package:provider/provider.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();
void main() async { void main() async {
await initializeDownloader(); ;
runApp(const MyApp());
}
// Setup the FlutterDownloader plugin
Future<void> initializeDownloader() async {
// Make sure FlutterDownloader can be used
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await FlutterDownloader.initialize(); runApp(MultiProvider(
// Set up the status update callback for FlutterDownloader providers: [
FlutterDownloader.registerCallback(downloadCallbackBackground); Provider(
// The actual callback is in the background isolate create: (context) => APKService(),
// So setup a port to pass the data to a foreground callback dispose: (context, apkInstallService) => apkInstallService.dispose(),
IsolateNameServer.registerPortWithName( ),
_port.sendPort, 'downloader_send_port'); ],
_port.listen((dynamic data) { child: const MyApp(),
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,
);
} }
// Extract a GitHub project name and author account name from a GitHub URL (can be any sub-URL of the project) // 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<String, String>? getAppNamesFromGitHubURL(String url) {
return null; return null;
} }
// Future<Directory> getAPKDir() async {
// var apkDir = Directory("${(await getExternalStorageDirectory())!.path}/apks");
// if (!apkDir.existsSync()) {
// apkDir.createSync();
// }
// return apkDir;
// }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@ -144,7 +83,9 @@ class _MyHomePageState extends State<MyHomePage> {
onPressed: () { onPressed: () {
var names = getAppNamesFromGitHubURL(urls[ind]); var names = getAppNamesFromGitHubURL(urls[ind]);
if (names != null) { if (names != null) {
downloadAPK(urls[ind], "${names["author"]!}_${names["appName"]!}"); Provider.of<APKService>(context, listen: false)
.downloadAndInstallAPK(
urls[ind], "${names["author"]!}_${names["appName"]!}");
setState(() { setState(() {
ind = ind == (urls.length - 1) ? 0 : ind + 1; ind = ind == (urls.length - 1) ? 0 : ind + 1;
}); });
@ -158,8 +99,6 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
void dispose() { void dispose() {
// Remove the FlutterDownloader communication port
IsolateNameServer.removePortNameMapping('downloader_send_port');
super.dispose(); super.dispose();
} }
} }

View File

@ -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<FGBGType>? foregroundSubscription;
// Setup the FlutterDownloader plugin (call in main())
Future<void> 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,
);
}
}

View File

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -50,6 +57,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
dbus:
dependency: transitive
description:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.7"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -90,6 +104,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -97,6 +118,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -156,6 +198,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -247,6 +296,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -268,6 +324,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -315,6 +378,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.12" version: "0.4.12"
timezone:
dependency: transitive
description:
name: timezone
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -336,6 +406,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0+1" version: "0.2.0+1"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
sdks: sdks:
dart: ">=2.19.0-79.0.dev <3.0.0" dart: ">=2.19.0-79.0.dev <3.0.0"
flutter: ">=3.0.0" flutter: ">=3.0.0"

View File

@ -42,6 +42,9 @@ dependencies:
path_provider: ^2.0.11 path_provider: ^2.0.11
flutter_downloader: ^1.8.1 flutter_downloader: ^1.8.1
app_installer: ^1.1.0 app_installer: ^1.1.0
flutter_fgbg: ^0.2.0
flutter_local_notifications: ^9.7.0
provider: ^6.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: