mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 05:16:43 +02:00
APK Service is ready
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -42,3 +42,6 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Custom
|
||||
TODO.txt
|
19
README.md
19
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.
|
32
android/app/proguard-rules.pro
vendored
Normal file
32
android/app/proguard-rules.pro
vendored
Normal 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 ----------
|
BIN
android/app/src/main/res/drawable/ic_launcher.png
Normal file
BIN
android/app/src/main/res/drawable/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 B |
@ -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<void> 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<String, String>? getAppNamesFromGitHubURL(String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Future<Directory> 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<MyHomePage> {
|
||||
onPressed: () {
|
||||
var names = getAppNamesFromGitHubURL(urls[ind]);
|
||||
if (names != null) {
|
||||
downloadAPK(urls[ind], "${names["author"]!}_${names["appName"]!}");
|
||||
Provider.of<APKService>(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<MyHomePage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Remove the FlutterDownloader communication port
|
||||
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
112
lib/services/apk_service.dart
Normal file
112
lib/services/apk_service.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
77
pubspec.lock
77
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"
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user