From b291c800f13d3862c60b834125b4254314972738 Mon Sep 17 00:00:00 2001 From: Gregory Date: Sun, 24 Dec 2023 18:26:11 +0300 Subject: [PATCH] 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}); } }