From de67e40c004d69b3c9a51c58c1eac19c4860f0c9 Mon Sep 17 00:00:00 2001 From: Gregory Date: Wed, 20 Dec 2023 11:57:56 +0300 Subject: [PATCH 1/6] Add installMethod in settings --- assets/translations/en.json | 4 ++++ assets/translations/ru.json | 4 ++++ lib/pages/settings.dart | 25 +++++++++++++++++++++++++ lib/providers/settings_provider.dart | 12 ++++++++++++ 4 files changed, 45 insertions(+) diff --git a/assets/translations/en.json b/assets/translations/en.json index cb061cf..54a4a5e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -278,6 +278,10 @@ "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "supportFixedAPKURL": "Support fixed APK URLs", "selectX": "Select {}", + "installMethod": "Installation method", + "normal": "Normal", + "shizuku": "Shizuku", + "root": "Root", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/ru.json b/assets/translations/ru.json index b0dfae8..a4eba6b 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -278,6 +278,10 @@ "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "supportFixedAPKURL": "Support fixed APK URLs", "selectX": "Select {}", + "installMethod": "Метод установки", + "normal": "Нормальный", + "shizuku": "Shizuku", + "root": "Суперпользователь", "removeAppQuestion": { "one": "Удалить приложение?", "other": "Удалить приложения?" diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index c36c958..23f742b 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, @@ -327,6 +350,8 @@ class _SettingsPageState extends State { }) ], ), + height16, + installMethodDropdown, height32, Text( tr('sourceSpecific'), diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 25e9a3b..cdc3b46 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]; From de60c4ee9ee6b7bbdff9055f08ad66609d4e1ba5 Mon Sep 17 00:00:00 2001 From: Gregory Date: Wed, 20 Dec 2023 16:21:12 +0300 Subject: [PATCH 2/6] Root install --- android/app/build.gradle | 6 ++++ .../com/example/obtainium/MainActivity.kt | 33 +++++++++++++++++++ lib/providers/apps_provider.dart | 11 +++++-- lib/providers/installers_provider.dart | 14 ++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 lib/providers/installers_provider.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 90500f3..32c3bdd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,6 +90,12 @@ flutter { source '../..' } +repositories { + maven { url 'https://jitpack.io' } +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + def libsuVersion = '5.2.2' + implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" } diff --git a/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt b/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt index 7693e63..8fa95c1 100644 --- a/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt @@ -1,6 +1,39 @@ package dev.imranr.obtainium 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 androidx.annotation.NonNull +import com.topjohnwu.superuser.Shell class MainActivity: FlutterActivity() { + private val installersChannel = "installers" + + private fun installWithRoot(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(if (builder.toString().endsWith("Success")) 0 else 1) + } + } + + private fun installWithShizuku(apkFilePath: String, result: Result) { + val a = 1 + } + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installersChannel).setMethodCallHandler { + call, result -> + var apkFilePath: String? = call.argument("apkFilePath") + if (call.method == "installWithShizuku") { + installWithShizuku(apkFilePath.toString(), result) + } else if (call.method == "installWithRoot") { + installWithRoot(apkFilePath.toString(), result) + } + } + } } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 35fbfcc..b8f0b48 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(); @@ -515,8 +516,14 @@ class AppsProvider with ChangeNotifier { await saveApps([apps[file.appId]!.app], attemptToCorrectInstallStatus: false); } - int? code = - await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); + int? code; + if (settingsProvider.installMethod == InstallMethodSettings.normal) { + code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); + } else if (settingsProvider.installMethod == InstallMethodSettings.shizuku) { + code = await Installers.installWithShizuku(apkFilePath: file.file.path); + } else if (settingsProvider.installMethod == InstallMethodSettings.root) { + code = await Installers.installWithRoot(apkFilePath: file.file.path); + } bool installed = false; if (code != null && code != 0 && code != 3) { throw InstallError(code); diff --git a/lib/providers/installers_provider.dart b/lib/providers/installers_provider.dart new file mode 100644 index 0000000..d9c8f93 --- /dev/null +++ b/lib/providers/installers_provider.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; + +class Installers { + static const MethodChannel _channel = MethodChannel('installers'); + + static Future installWithShizuku({required String apkFilePath}) async { + return await _channel.invokeMethod('installWithShizuku', {'apkFilePath': apkFilePath}); + } + + static Future installWithRoot({required String apkFilePath}) async { + return await _channel.invokeMethod('installWithRoot', {'apkFilePath': apkFilePath}); + } +} From 36e6c267b9efb742b0886b972d13294aad14b0d3 Mon Sep 17 00:00:00 2001 From: Gregory Date: Fri, 22 Dec 2023 14:21:18 +0300 Subject: [PATCH 3/6] Shizuku dependencies --- android/app/build.gradle | 23 +++++++++++++++---- .../imranr}/obtainium/MainActivity.kt | 0 android/build.gradle | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) rename android/app/src/main/kotlin/{com/example => dev/imranr}/obtainium/MainActivity.kt (100%) diff --git a/android/app/build.gradle b/android/app/build.gradle index 32c3bdd..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 } @@ -96,6 +97,18 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - def libsuVersion = '5.2.2' - implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" + + 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/kotlin/com/example/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/example/obtainium/MainActivity.kt rename to android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt 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! } } From b6b8db48df98821692a31516b92002c2289c021d Mon Sep 17 00:00:00 2001 From: Gregory Date: Fri, 22 Dec 2023 16:08:41 +0300 Subject: [PATCH 4/6] request Shizuku permission --- android/app/src/main/AndroidManifest.xml | 7 +++ .../dev/imranr/obtainium/MainActivity.kt | 59 +++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) 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/dev/imranr/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt index 8fa95c1..7e68355 100644 --- a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt @@ -5,10 +5,51 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.Result import androidx.annotation.NonNull +import android.content.pm.PackageManager +import android.os.Bundle +import rikka.shizuku.Shizuku +import rikka.shizuku.Shizuku.OnBinderDeadListener +import rikka.shizuku.Shizuku.OnBinderReceivedListener +import rikka.shizuku.Shizuku.OnRequestPermissionResultListener import com.topjohnwu.superuser.Shell class MainActivity: FlutterActivity() { private val installersChannel = "installers" + private val SHIZUKU_PERMISSION_REQUEST_CODE = 839 // random num + private var shizukuBinderAlive = false + private var shizukuPermissionGranted = false + + private val shizukuBinderReceivedListener = OnBinderReceivedListener { + if(!Shizuku.isPreV11()) { // pre 11 unsupported + shizukuBinderAlive = true + } + } + + private val shizukuBinderDeadListener = OnBinderDeadListener { shizukuBinderAlive = false } + + private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener { + requestCode: Int, grantResult: Int -> + if(requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { + shizukuPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED + } + } + + private fun shizukuCheckPermission() { + if(Shizuku.isPreV11()) { + shizukuPermissionGranted = false + } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + shizukuPermissionGranted = true + } else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again + shizukuPermissionGranted = false + } else { + Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) + } + } + + private fun installWithShizuku(apkFilePath: String, result: Result) { + shizukuCheckPermission() + result.success(0) + } private fun installWithRoot(apkFilePath: String, result: Result) { Shell.sh("pm install -r -t " + apkFilePath).submit { out -> @@ -20,10 +61,6 @@ class MainActivity: FlutterActivity() { } } - private fun installWithShizuku(apkFilePath: String, result: Result) { - val a = 1 - } - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installersChannel).setMethodCallHandler { @@ -36,4 +73,18 @@ class MainActivity: FlutterActivity() { } } } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Shizuku.addBinderReceivedListener(shizukuBinderReceivedListener) + Shizuku.addBinderDeadListener(shizukuBinderDeadListener) + Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener) + } + + override fun onDestroy() { + super.onDestroy() + Shizuku.removeBinderReceivedListener(shizukuBinderReceivedListener) + Shizuku.removeBinderDeadListener(shizukuBinderDeadListener) + Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener) + } } From 375b9bce30bf3b0cac7c14d4cbff26898d8412dd Mon Sep 17 00:00:00 2001 From: Gregory Date: Fri, 22 Dec 2023 16:27:54 +0300 Subject: [PATCH 5/6] Working shizuku installer, need refactor --- .../dev/imranr/obtainium/MainActivity.kt | 114 ++++++++++++++++++ .../obtainium/util/ApplicationUtils.java | 37 ++++++ .../obtainium/util/IIntentSenderAdaptor.java | 23 ++++ .../obtainium/util/IntentSenderUtils.java | 14 +++ .../obtainium/util/PackageInstallerUtils.java | 41 +++++++ .../util/ShizukuSystemServerApi.java | 68 +++++++++++ .../dev/imranr/obtainium/util/Singleton.java | 17 +++ 7 files changed, 314 insertions(+) create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java create mode 100644 android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt index 7e68355..57fe607 100644 --- a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt @@ -5,12 +5,27 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.Result import androidx.annotation.NonNull +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 rikka.shizuku.Shizuku import rikka.shizuku.Shizuku.OnBinderDeadListener import rikka.shizuku.Shizuku.OnBinderReceivedListener import rikka.shizuku.Shizuku.OnRequestPermissionResultListener +import rikka.shizuku.ShizukuBinderWrapper +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 java.io.IOException +import java.util.concurrent.CountDownLatch import com.topjohnwu.superuser.Shell class MainActivity: FlutterActivity() { @@ -46,8 +61,107 @@ class MainActivity: FlutterActivity() { } } + private fun shizukuInstallApk(uri: Uri) { + val packageInstaller: PackageInstaller + var session: PackageInstaller.Session? = null + val cr = contentResolver + val res = StringBuilder() + val installerPackageName: String + var installerAttributionTag: String? = null + val userId: Int + val isRoot: Boolean + try { + val _packageInstaller: IPackageInstaller = + ShizukuSystemServerApi.PackageManager_getPackageInstaller() + 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 + installerPackageName = if (isRoot) packageName else "com.android.shell" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + installerAttributionTag = attributionTag + } + userId = if (isRoot) Process.myUserHandle().hashCode() else 0 + packageInstaller = PackageInstallerUtils.createPackageInstaller( + _packageInstaller, + installerPackageName, + installerAttributionTag, + userId + ) + val sessionId: Int + res.append("createSession: ") + val params = + PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + var installFlags: Int = PackageInstallerUtils.getInstallFlags(params) + installFlags = + installFlags or (0x00000004 /*PackageManager.INSTALL_ALLOW_TEST*/ or 0x00000002) /*PackageManager.INSTALL_REPLACE_EXISTING*/ + PackageInstallerUtils.setInstallFlags(params, installFlags) + sessionId = packageInstaller.createSession(params) + res.append(sessionId).append('\n') + res.append('\n').append("write: ") + val _session = IPackageInstallerSession.Stub.asInterface( + ShizukuBinderWrapper( + _packageInstaller.openSession(sessionId).asBinder() + ) + ) + session = PackageInstallerUtils.createSession(_session) + val name = "apk.apk" + val `is` = cr.openInputStream(uri) + val os = session.openWrite(name, 0, -1) + val buf = ByteArray(8192) + var len: Int + try { + while (`is`!!.read(buf).also { len = it } > 0) { + os.write(buf, 0, len) + os.flush() + session.fsync(os) + } + } finally { + try { + `is`!!.close() + } catch (e: IOException) { + e.printStackTrace() + } + try { + os.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + res.append('\n').append("commit: ") + 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() + val result = results[0] + val status = + result!!.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + val message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + res.append('\n').append("status: ").append(status).append(" (").append(message) + .append(")") + } catch (tr: Throwable) { + tr.printStackTrace() + res.append(tr) + } finally { + if (session != null) { + try { + session.close() + } catch (tr: Throwable) { + res.append(tr) + } + } + } + } + private fun installWithShizuku(apkFilePath: String, result: Result) { shizukuCheckPermission() + shizukuInstallApk(Uri.parse("file://$apkFilePath")) result.success(0) } 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; + } + } +} From b291c800f13d3862c60b834125b4254314972738 Mon Sep 17 00:00:00 2001 From: Gregory Date: Sun, 24 Dec 2023 18:26:11 +0300 Subject: [PATCH 6/6] Now it looks good --- .../dev/imranr/obtainium/MainActivity.kt | 207 ++++++++---------- assets/translations/en.json | 1 + assets/translations/ru.json | 7 +- lib/providers/apps_provider.dart | 34 ++- lib/providers/installers_provider.dart | 48 +++- 5 files changed, 161 insertions(+), 136 deletions(-) diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt index 57fe607..72c22cb 100644 --- a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt @@ -1,10 +1,5 @@ package dev.imranr.obtainium -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 androidx.annotation.NonNull import android.content.Intent import android.content.IntentSender import android.content.pm.IPackageInstaller @@ -15,119 +10,95 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Process -import rikka.shizuku.Shizuku -import rikka.shizuku.Shizuku.OnBinderDeadListener -import rikka.shizuku.Shizuku.OnBinderReceivedListener -import rikka.shizuku.Shizuku.OnRequestPermissionResultListener -import rikka.shizuku.ShizukuBinderWrapper +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 com.topjohnwu.superuser.Shell +import rikka.shizuku.Shizuku +import rikka.shizuku.Shizuku.OnRequestPermissionResultListener +import rikka.shizuku.ShizukuBinderWrapper class MainActivity: FlutterActivity() { - private val installersChannel = "installers" - private val SHIZUKU_PERMISSION_REQUEST_CODE = 839 // random num - private var shizukuBinderAlive = false - private var shizukuPermissionGranted = false + private var installersChannel: MethodChannel? = null + private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random() - private val shizukuBinderReceivedListener = OnBinderReceivedListener { - if(!Shizuku.isPreV11()) { // pre 11 unsupported - shizukuBinderAlive = true + private fun shizukuCheckPermission() { + try { + if (Shizuku.isPreV11()) { // Unsupported + installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to -1)) + } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { + installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to 1)) + } else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again + installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to 0)) + } else { + Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) + } + } catch (_: Exception) { // If shizuku not running + installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to -1)) } } - private val shizukuBinderDeadListener = OnBinderDeadListener { shizukuBinderAlive = false } - private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener { requestCode: Int, grantResult: Int -> - if(requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { - shizukuPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED + 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 shizukuCheckPermission() { - if(Shizuku.isPreV11()) { - shizukuPermissionGranted = false - } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { - shizukuPermissionGranted = true - } else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again - shizukuPermissionGranted = false - } else { - Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) - } - } - - private fun shizukuInstallApk(uri: Uri) { - val packageInstaller: PackageInstaller + private fun shizukuInstallApk(apkFileUri: String, result: Result) { + val uri = Uri.parse(apkFileUri) + var res = false var session: PackageInstaller.Session? = null - val cr = contentResolver - val res = StringBuilder() - val installerPackageName: String - var installerAttributionTag: String? = null - val userId: Int - val isRoot: Boolean try { - val _packageInstaller: IPackageInstaller = + val iPackageInstaller: IPackageInstaller = ShizukuSystemServerApi.PackageManager_getPackageInstaller() - 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 - installerPackageName = if (isRoot) packageName else "com.android.shell" + 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 } - userId = if (isRoot) Process.myUserHandle().hashCode() else 0 - packageInstaller = PackageInstallerUtils.createPackageInstaller( - _packageInstaller, - installerPackageName, - installerAttributionTag, - userId - ) - val sessionId: Int - res.append("createSession: ") + 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*/ or 0x00000002) /*PackageManager.INSTALL_REPLACE_EXISTING*/ + installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST PackageInstallerUtils.setInstallFlags(params, installFlags) - sessionId = packageInstaller.createSession(params) - res.append(sessionId).append('\n') - res.append('\n').append("write: ") - val _session = IPackageInstallerSession.Stub.asInterface( - ShizukuBinderWrapper( - _packageInstaller.openSession(sessionId).asBinder() - ) - ) - session = PackageInstallerUtils.createSession(_session) - val name = "apk.apk" - val `is` = cr.openInputStream(uri) - val os = session.openWrite(name, 0, -1) - val buf = ByteArray(8192) - var len: Int + 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 (`is`!!.read(buf).also { len = it } > 0) { - os.write(buf, 0, len) - os.flush() - session.fsync(os) + while (inputStream!!.read(buffer).also { length = it } > 0) { + openedSession.write(buffer, 0, length) + openedSession.flush() + session.fsync(openedSession) } } finally { try { - `is`!!.close() - } catch (e: IOException) { - e.printStackTrace() - } - try { - os.close() + inputStream!!.close() + openedSession.close() } catch (e: IOException) { e.printStackTrace() } } - res.append('\n').append("commit: ") val results = arrayOf(null) val countDownLatch = CountDownLatch(1) val intentSender: IntentSender = @@ -139,66 +110,62 @@ class MainActivity: FlutterActivity() { }) session.commit(intentSender) countDownLatch.await() - val result = results[0] - val status = - result!!.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) - val message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - res.append('\n').append("status: ").append(status).append(" (").append(message) - .append(")") - } catch (tr: Throwable) { - tr.printStackTrace() - res.append(tr) + res = results[0]!!.getIntExtra( + PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0 + } catch (_: Exception) { + res = false } finally { if (session != null) { try { session.close() - } catch (tr: Throwable) { - res.append(tr) + } catch (_: Exception) { + res = false } } } + result.success(res) } - private fun installWithShizuku(apkFilePath: String, result: Result) { - shizukuCheckPermission() - shizukuInstallApk(Uri.parse("file://$apkFilePath")) - result.success(0) - } - - private fun installWithRoot(apkFilePath: String, result: Result) { - Shell.sh("pm install -r -t " + apkFilePath).submit { out -> - val builder = StringBuilder() - for (data in out.getOut()) { - builder.append(data) + private fun rootCheckPermission(result: Result) { + Shell.getShell(Shell.GetShellCallback( + fun(shell: Shell) { + result.success(shell.isRoot) } - result.success(if (builder.toString().endsWith("Success")) 0 else 1) + )) + } + + 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) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installersChannel).setMethodCallHandler { + Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener) + installersChannel = MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, "installers") + installersChannel!!.setMethodCallHandler { call, result -> - var apkFilePath: String? = call.argument("apkFilePath") - if (call.method == "installWithShizuku") { - installWithShizuku(apkFilePath.toString(), result) + if (call.method == "checkPermissionShizuku") { + shizukuCheckPermission() + result.success(0) + } 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") { - installWithRoot(apkFilePath.toString(), result) + val apkFilePath: String? = call.argument("apkFilePath") + rootInstallApk(apkFilePath!!, result) } } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Shizuku.addBinderReceivedListener(shizukuBinderReceivedListener) - Shizuku.addBinderDeadListener(shizukuBinderDeadListener) - Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener) - } - override fun onDestroy() { super.onDestroy() - Shizuku.removeBinderReceivedListener(shizukuBinderReceivedListener) - Shizuku.removeBinderDeadListener(shizukuBinderDeadListener) Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener) } } diff --git a/assets/translations/en.json b/assets/translations/en.json index 54a4a5e..251c19e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -282,6 +282,7 @@ "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 a4eba6b..c59a501 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -275,13 +275,14 @@ "downloadingXNotifChannel": "Загрузка {}", "completeAppInstallationNotifChannel": "Завершение установки приложения", "checkingForUpdatesNotifChannel": "Проверка обновлений", - "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", - "supportFixedAPKURL": "Support fixed APK URLs", - "selectX": "Select {}", + "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений", + "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK", + "selectX": "Выбрать {}", "installMethod": "Метод установки", "normal": "Нормальный", "shizuku": "Shizuku", "root": "Суперпользователь", + "shizukuBinderNotFound": "Shizuku не запущен", "removeAppQuestion": { "one": "Удалить приложение?", "other": "Удалить приложения?" diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b8f0b48..6d06c82 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -505,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 @@ -517,12 +518,13 @@ class AppsProvider with ChangeNotifier { attemptToCorrectInstallStatus: false); } int? code; - if (settingsProvider.installMethod == InstallMethodSettings.normal) { - code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); - } else if (settingsProvider.installMethod == InstallMethodSettings.shizuku) { - code = await Installers.installWithShizuku(apkFilePath: file.file.path); - } else if (settingsProvider.installMethod == InstallMethodSettings.root) { - code = await Installers.installWithRoot(apkFilePath: file.file.path); + 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) { @@ -679,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 index d9c8f93..b8ecd2e 100644 --- a/lib/providers/installers_provider.dart +++ b/lib/providers/installers_provider.dart @@ -3,12 +3,52 @@ 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 installWithShizuku({required String apkFilePath}) async { - return await _channel.invokeMethod('installWithShizuku', {'apkFilePath': apkFilePath}); + static Future waitWhile(bool Function() test, + [Duration pollInterval = const Duration(milliseconds: 100)]) { + var completer = Completer(); + check() { + if (test()) { + Timer(pollInterval, check); + } else { + completer.complete(); + } + } + check(); + return completer.future; } - static Future installWithRoot({required String apkFilePath}) async { - return await _channel.invokeMethod('installWithRoot', {'apkFilePath': apkFilePath}); + 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; + } + await _channel.invokeMethod('checkPermissionShizuku'); + await waitWhile(() => _resPermShizuku == -2); + int 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}); } }