diff --git a/android/app/build.gradle b/android/app/build.gradle
index 90500f3..2559d75 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
+apply plugin: 'dev.rikka.tools.refine'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
@@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdkVersion 33
+ compileSdkVersion 34
ndkVersion flutter.ndkVersion
compileOptions {
@@ -52,8 +53,8 @@ android {
applicationId "dev.imranr.obtainium"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
- minSdkVersion 23
- targetSdkVersion 33
+ minSdkVersion 24
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -90,6 +91,24 @@ flutter {
source '../..'
}
+repositories {
+ maven { url 'https://jitpack.io' }
+}
+
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ def shizuku_version = '13.1.5'
+ implementation "dev.rikka.shizuku:api:$shizuku_version"
+ implementation "dev.rikka.shizuku:provider:$shizuku_version"
+
+ def hidden_api_version = '4.1.0'
+ // DO NOT UPDATE Hidden API without updating the Android tools
+ // and do not update Android tools without updating the whole Flutter
+ // (also in android/build.gradle)
+ implementation "dev.rikka.tools.refine:runtime:$hidden_api_version"
+ implementation "dev.rikka.hidden:compat:$hidden_api_version"
+ compileOnly "dev.rikka.hidden:stub:$hidden_api_version"
+
+ implementation "com.github.topjohnwu.libsu:core:5.2.2"
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fb0c563..503cd67 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -66,6 +66,13 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
diff --git a/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt b/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt
deleted file mode 100644
index 7693e63..0000000
--- a/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package dev.imranr.obtainium
-
-import io.flutter.embedding.android.FlutterActivity
-
-class MainActivity: FlutterActivity() {
-}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
new file mode 100644
index 0000000..af2da43
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
@@ -0,0 +1,171 @@
+package dev.imranr.obtainium
+
+import android.content.Intent
+import android.content.IntentSender
+import android.content.pm.IPackageInstaller
+import android.content.pm.IPackageInstallerSession
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Process
+import androidx.annotation.NonNull
+import com.topjohnwu.superuser.Shell
+import dev.imranr.obtainium.util.IIntentSenderAdaptor
+import dev.imranr.obtainium.util.IntentSenderUtils
+import dev.imranr.obtainium.util.PackageInstallerUtils
+import dev.imranr.obtainium.util.ShizukuSystemServerApi
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.Result
+import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import rikka.shizuku.Shizuku
+import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
+import rikka.shizuku.ShizukuBinderWrapper
+
+class MainActivity: FlutterActivity() {
+ private var installersChannel: MethodChannel? = null
+ private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
+
+ private fun shizukuCheckPermission(result: Result) {
+ try {
+ if (Shizuku.isPreV11()) { // Unsupported
+ result.success(-1)
+ } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
+ result.success(1)
+ } else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
+ result.success(0)
+ } else {
+ Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
+ result.success(-2)
+ }
+ } catch (_: Exception) { // If shizuku not running
+ result.success(-1)
+ }
+ }
+
+ private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
+ requestCode: Int, grantResult: Int ->
+ if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
+ val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
+ installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
+ }
+ }
+
+ private fun shizukuInstallApk(apkFileUri: String, result: Result) {
+ val uri = Uri.parse(apkFileUri)
+ var res = false
+ var session: PackageInstaller.Session? = null
+ try {
+ val iPackageInstaller: IPackageInstaller =
+ ShizukuSystemServerApi.PackageManager_getPackageInstaller()
+ val isRoot = Shizuku.getUid() == 0
+ // The reason for use "com.android.shell" as installer package under adb
+ // is that getMySessions will check installer package's owner
+ val installerPackageName = if (isRoot) packageName else "com.android.shell"
+ var installerAttributionTag: String? = null
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ installerAttributionTag = attributionTag
+ }
+ val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
+ val packageInstaller = PackageInstallerUtils.createPackageInstaller(
+ iPackageInstaller, installerPackageName, installerAttributionTag, userId)
+ val params =
+ PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
+ installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
+ PackageInstallerUtils.setInstallFlags(params, installFlags)
+ val sessionId = packageInstaller.createSession(params)
+ val iSession = IPackageInstallerSession.Stub.asInterface(
+ ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
+ session = PackageInstallerUtils.createSession(iSession)
+ val inputStream = contentResolver.openInputStream(uri)
+ val openedSession = session.openWrite("apk.apk", 0, -1)
+ val buffer = ByteArray(8192)
+ var length: Int
+ try {
+ while (inputStream!!.read(buffer).also { length = it } > 0) {
+ openedSession.write(buffer, 0, length)
+ openedSession.flush()
+ session.fsync(openedSession)
+ }
+ } finally {
+ try {
+ inputStream!!.close()
+ openedSession.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ val results = arrayOf(null)
+ val countDownLatch = CountDownLatch(1)
+ val intentSender: IntentSender =
+ IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() {
+ override fun send(intent: Intent?) {
+ results[0] = intent
+ countDownLatch.countDown()
+ }
+ })
+ session.commit(intentSender)
+ countDownLatch.await()
+ res = results[0]!!.getIntExtra(
+ PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
+ } catch (_: Exception) {
+ res = false
+ } finally {
+ if (session != null) {
+ try {
+ session.close()
+ } catch (_: Exception) {
+ res = false
+ }
+ }
+ }
+ result.success(res)
+ }
+
+ private fun rootCheckPermission(result: Result) {
+ Shell.getShell(Shell.GetShellCallback(
+ fun(shell: Shell) {
+ result.success(shell.isRoot)
+ }
+ ))
+ }
+
+ private fun rootInstallApk(apkFilePath: String, result: Result) {
+ Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
+ val builder = StringBuilder()
+ for (data in out.getOut()) { builder.append(data) }
+ result.success(builder.toString().endsWith("Success"))
+ }
+ }
+
+ override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
+ installersChannel = MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger, "installers")
+ installersChannel!!.setMethodCallHandler {
+ call, result ->
+ if (call.method == "checkPermissionShizuku") {
+ shizukuCheckPermission(result)
+ } else if (call.method == "checkPermissionRoot") {
+ rootCheckPermission(result)
+ } else if (call.method == "installWithShizuku") {
+ val apkFileUri: String? = call.argument("apkFileUri")
+ shizukuInstallApk(apkFileUri!!, result)
+ } else if (call.method == "installWithRoot") {
+ val apkFilePath: String? = call.argument("apkFilePath")
+ rootInstallApk(apkFilePath!!, result)
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
+ }
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java
new file mode 100644
index 0000000..6aa7489
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java
@@ -0,0 +1,37 @@
+package dev.imranr.obtainium.util;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.os.Build;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ApplicationUtils {
+
+ private static Application application;
+
+ public static Application getApplication() {
+ return application;
+ }
+
+ public static void setApplication(Application application) {
+ ApplicationUtils.application = application;
+ }
+
+ public static String getProcessName() {
+ if (Build.VERSION.SDK_INT >= 28)
+ return Application.getProcessName();
+ else {
+ try {
+ @SuppressLint("PrivateApi")
+ Class> activityThread = Class.forName("android.app.ActivityThread");
+ @SuppressLint("DiscouragedPrivateApi")
+ Method method = activityThread.getDeclaredMethod("currentProcessName");
+ return (String) method.invoke(null);
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java
new file mode 100644
index 0000000..2178c77
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java
@@ -0,0 +1,23 @@
+package dev.imranr.obtainium.util;
+
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+public abstract class IIntentSenderAdaptor extends IIntentSender.Stub {
+
+ public abstract void send(Intent intent);
+
+ @Override
+ public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+ send(intent);
+ return 0;
+ }
+
+ @Override
+ public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+ send(intent);
+ }
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java
new file mode 100644
index 0000000..ab6acba
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java
@@ -0,0 +1,14 @@
+package dev.imranr.obtainium.util;
+
+import android.content.IIntentSender;
+import android.content.IntentSender;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class IntentSenderUtils {
+
+ public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ //noinspection JavaReflectionMemberAccess
+ return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder);
+ }
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java
new file mode 100644
index 0000000..9d5ae14
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java
@@ -0,0 +1,41 @@
+package dev.imranr.obtainium.util;
+
+import android.content.Context;
+import android.content.pm.IPackageInstaller;
+import android.content.pm.IPackageInstallerSession;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import java.lang.reflect.InvocationTargetException;
+
+@SuppressWarnings({"JavaReflectionMemberAccess"})
+public class PackageInstallerUtils {
+
+ public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class)
+ .newInstance(installer, installerPackageName, installerAttributionTag, userId);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class)
+ .newInstance(installer, installerPackageName, userId);
+ } else {
+ return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class)
+ .newInstance(ApplicationUtils.getApplication(), ApplicationUtils.getApplication().getPackageManager(), installer, installerPackageName, userId);
+ }
+ }
+
+ public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class)
+ .newInstance(session);
+
+ }
+
+ public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException {
+ return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params);
+ }
+
+ public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException {
+ PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue);
+ }
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java
new file mode 100644
index 0000000..6dcdf75
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java
@@ -0,0 +1,68 @@
+package dev.imranr.obtainium.util;
+
+import android.content.Context;
+import android.content.pm.IPackageInstaller;
+import android.content.pm.IPackageManager;
+import android.content.pm.UserInfo;
+import android.os.Build;
+import android.os.IUserManager;
+import android.os.RemoteException;
+
+import java.util.List;
+
+import rikka.shizuku.ShizukuBinderWrapper;
+import rikka.shizuku.SystemServiceHelper;
+
+public class ShizukuSystemServerApi {
+
+ private static final Singleton PACKAGE_MANAGER = new Singleton() {
+ @Override
+ protected IPackageManager create() {
+ return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));
+ }
+ };
+
+ private static final Singleton USER_MANAGER = new Singleton() {
+ @Override
+ protected IUserManager create() {
+ return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE)));
+ }
+ };
+
+ public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException {
+ IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller();
+ return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder()));
+ }
+
+ public static List UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException {
+ if (Build.VERSION.SDK_INT >= 30) {
+ return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
+ } else {
+ try {
+ return USER_MANAGER.get().getUsers(excludeDying);
+ } catch (NoSuchFieldError e) {
+ return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
+ }
+ }
+ }
+
+ // method 2: use transactRemote directly
+ /*public static List UserManager_getUsers(boolean excludeDying) {
+ Parcel data = SystemServiceHelper.obtainParcel(Context.USER_SERVICE, "android.os.IUserManager", "getUsers");
+ Parcel reply = Parcel.obtain();
+ data.writeInt(excludeDying ? 1 : 0);
+
+ List res = null;
+ try {
+ ShizukuService.transactRemote(data, reply, 0);
+ reply.readException();
+ res = reply.createTypedArrayList(UserInfo.CREATOR);
+ } catch (RemoteException e) {
+ Log.e("ShizukuSample", "UserManager#getUsers", e);
+ } finally {
+ data.recycle();
+ reply.recycle();
+ }
+ return res;
+ }*/
+}
diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java
new file mode 100644
index 0000000..e535245
--- /dev/null
+++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java
@@ -0,0 +1,17 @@
+package dev.imranr.obtainium.util;
+
+public abstract class Singleton {
+
+ private T mInstance;
+
+ protected abstract T create();
+
+ public final T get() {
+ synchronized (this) {
+ if (mInstance == null) {
+ mInstance = create();
+ }
+ return mInstance;
+ }
+ }
+}
diff --git a/android/build.gradle b/android/build.gradle
index 713d7f6..288d38b 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'dev.rikka.tools.refine:gradle-plugin:4.1.0' // Do not update!
}
}
diff --git a/assets/translations/en.json b/assets/translations/en.json
index fef8f24..defd488 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -281,6 +281,11 @@
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
+ "installMethod": "Installation method",
+ "normal": "Normal",
+ "shizuku": "Shizuku",
+ "root": "Root",
+ "shizukuBinderNotFound": "Shizuku is not running",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"
diff --git a/assets/translations/ru.json b/assets/translations/ru.json
index 03a73e2..fee1a28 100644
--- a/assets/translations/ru.json
+++ b/assets/translations/ru.json
@@ -277,10 +277,15 @@
"downloadingXNotifChannel": "Загрузка {}",
"completeAppInstallationNotifChannel": "Завершение установки приложения",
"checkingForUpdatesNotifChannel": "Проверка обновлений",
- "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
- "supportFixedAPKURL": "Support fixed APK URLs",
- "selectX": "Select {}",
- "parallelDownloads": "Allow parallel downloads",
+ "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
+ "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
+ "selectX": "Выбрать {}",
+ "parallelDownloads": "Разрешить параллельные загрузки",
+ "installMethod": "Метод установки",
+ "normal": "Нормальный",
+ "shizuku": "Shizuku",
+ "root": "Суперпользователь",
+ "shizukuBinderNotFound": "Shizuku не запущен",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"
diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart
index d0cd11b..e83ddf3 100644
--- a/lib/pages/settings.dart
+++ b/lib/pages/settings.dart
@@ -30,6 +30,29 @@ class _SettingsPageState extends State {
settingsProvider.initializeSettings();
}
+ var installMethodDropdown = DropdownButtonFormField(
+ decoration: InputDecoration(labelText: tr('installMethod')),
+ value: settingsProvider.installMethod,
+ items: [
+ DropdownMenuItem(
+ value: InstallMethodSettings.normal,
+ child: Text(tr('normal')),
+ ),
+ DropdownMenuItem(
+ value: InstallMethodSettings.shizuku,
+ child: Text(tr('shizuku')),
+ ),
+ DropdownMenuItem(
+ value: InstallMethodSettings.root,
+ child: Text(tr('root')),
+ )
+ ],
+ onChanged: (value) {
+ if (value != null) {
+ settingsProvider.installMethod = value;
+ }
+ });
+
var themeDropdown = DropdownButtonFormField(
decoration: InputDecoration(labelText: tr('theme')),
value: settingsProvider.theme,
@@ -328,6 +351,8 @@ class _SettingsPageState extends State {
],
),
height16,
+ installMethodDropdown,
+ height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart
index df90ff6..2669a0a 100644
--- a/lib/providers/apps_provider.dart
+++ b/lib/providers/apps_provider.dart
@@ -33,6 +33,7 @@ import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
+import 'installers_provider.dart';
final pm = AndroidPackageManager();
@@ -504,7 +505,8 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
- if (needsBGWorkaround) {
+ if (needsBGWorkaround &&
+ settingsProvider.installMethod == InstallMethodSettings.normal) {
// The below 'await' will never return if we are in a background process
// To work around this, we should assume the install will be successful
// So we update the app's installed version first as we will never get to the later code
@@ -515,8 +517,15 @@ class AppsProvider with ChangeNotifier {
await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false);
}
- int? code =
- await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
+ int? code;
+ switch (settingsProvider.installMethod) {
+ case InstallMethodSettings.normal:
+ code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
+ case InstallMethodSettings.shizuku:
+ code = (await Installers.installWithShizuku(apkFileUri: file.file.uri.toString())) ? 0 : 1;
+ case InstallMethodSettings.root:
+ code = (await Installers.installWithRoot(apkFilePath: file.file.path)) ? 0 : 1;
+ }
bool installed = false;
if (code != null && code != 0 && code != 3) {
throw InstallError(code);
@@ -672,8 +681,22 @@ class AppsProvider with ChangeNotifier {
}
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
- if (!(await settingsProvider.getInstallPermission(enforce: false))) {
- throw ObtainiumError(tr('cancelled'));
+ switch (settingsProvider.installMethod) {
+ case InstallMethodSettings.normal:
+ if (!(await settingsProvider.getInstallPermission(enforce: false))) {
+ throw ObtainiumError(tr('cancelled'));
+ }
+ case InstallMethodSettings.shizuku:
+ int code = await Installers.checkPermissionShizuku();
+ if (code == -1) {
+ throw ObtainiumError(tr('shizukuBinderNotFound'));
+ } else if (code == 0) {
+ throw ObtainiumError(tr('cancelled'));
+ }
+ case InstallMethodSettings.root:
+ if (!(await Installers.checkPermissionRoot())) {
+ throw ObtainiumError(tr('cancelled'));
+ }
}
if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously
diff --git a/lib/providers/installers_provider.dart b/lib/providers/installers_provider.dart
new file mode 100644
index 0000000..c42e1b0
--- /dev/null
+++ b/lib/providers/installers_provider.dart
@@ -0,0 +1,56 @@
+import 'dart:async';
+import 'package:flutter/services.dart';
+
+class Installers {
+ static const MethodChannel _channel = MethodChannel('installers');
+ static bool _callbacksApplied = false;
+ static int _resPermShizuku = -2; // not set
+
+ static Future waitWhile(bool Function() test,
+ [Duration pollInterval = const Duration(milliseconds: 250)]) {
+ var completer = Completer();
+ check() {
+ if (test()) {
+ Timer(pollInterval, check);
+ } else {
+ completer.complete();
+ }
+ }
+ check();
+ return completer.future;
+ }
+
+ static Future handleCalls(MethodCall call) async {
+ if (call.method == 'resPermShizuku') {
+ _resPermShizuku = call.arguments['res'];
+ }
+ }
+
+ static Future checkPermissionShizuku() async {
+ if (!_callbacksApplied) {
+ _channel.setMethodCallHandler(handleCalls);
+ _callbacksApplied = true;
+ }
+ int res = await _channel.invokeMethod('checkPermissionShizuku');
+ if(res == -2) {
+ await waitWhile(() => _resPermShizuku == -2);
+ res = _resPermShizuku;
+ _resPermShizuku = -2;
+ }
+ return res;
+ }
+
+ static Future checkPermissionRoot() async {
+ return await _channel.invokeMethod('checkPermissionRoot');
+ }
+
+ static Future installWithShizuku({required String apkFileUri}) async {
+ return await _channel.invokeMethod(
+ 'installWithShizuku', {'apkFileUri': apkFileUri});
+ }
+
+ static Future installWithRoot({required String apkFilePath}) async {
+ return await _channel.invokeMethod(
+ 'installWithRoot', {'apkFilePath': apkFilePath});
+ }
+}
diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart
index af7dd63..266f244 100644
--- a/lib/providers/settings_provider.dart
+++ b/lib/providers/settings_provider.dart
@@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
String obtainiumId = 'dev.imranr.obtainium';
+enum InstallMethodSettings { normal, shizuku, root }
+
enum ThemeSettings { system, light, dark }
enum ColourSettings { basic, materialYou }
@@ -49,6 +51,16 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
+ InstallMethodSettings get installMethod {
+ return InstallMethodSettings
+ .values[prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
+ }
+
+ set installMethod(InstallMethodSettings t) {
+ prefs?.setInt('installMethod', t.index);
+ notifyListeners();
+ }
+
ThemeSettings get theme {
return ThemeSettings
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];