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];