Now it looks good

This commit is contained in:
Gregory
2023-12-24 18:26:11 +03:00
parent 375b9bce30
commit b291c800f1
5 changed files with 161 additions and 136 deletions

View File

@ -1,10 +1,5 @@
package dev.imranr.obtainium 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.Intent
import android.content.IntentSender import android.content.IntentSender
import android.content.pm.IPackageInstaller import android.content.pm.IPackageInstaller
@ -15,119 +10,95 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import rikka.shizuku.Shizuku import androidx.annotation.NonNull
import rikka.shizuku.Shizuku.OnBinderDeadListener import com.topjohnwu.superuser.Shell
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.IIntentSenderAdaptor
import dev.imranr.obtainium.util.IntentSenderUtils import dev.imranr.obtainium.util.IntentSenderUtils
import dev.imranr.obtainium.util.PackageInstallerUtils import dev.imranr.obtainium.util.PackageInstallerUtils
import dev.imranr.obtainium.util.ShizukuSystemServerApi 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.io.IOException
import java.util.concurrent.CountDownLatch 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() { class MainActivity: FlutterActivity() {
private val installersChannel = "installers" private var installersChannel: MethodChannel? = null
private val SHIZUKU_PERMISSION_REQUEST_CODE = 839 // random num private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
private var shizukuBinderAlive = false
private var shizukuPermissionGranted = false
private val shizukuBinderReceivedListener = OnBinderReceivedListener { private fun shizukuCheckPermission() {
if(!Shizuku.isPreV11()) { // pre 11 unsupported try {
shizukuBinderAlive = true 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 { private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
requestCode: Int, grantResult: Int -> requestCode: Int, grantResult: Int ->
if(requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) { if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
shizukuPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
} }
} }
private fun shizukuCheckPermission() { private fun shizukuInstallApk(apkFileUri: String, result: Result) {
if(Shizuku.isPreV11()) { val uri = Uri.parse(apkFileUri)
shizukuPermissionGranted = false var res = 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
var session: PackageInstaller.Session? = null 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 { try {
val _packageInstaller: IPackageInstaller = val iPackageInstaller: IPackageInstaller =
ShizukuSystemServerApi.PackageManager_getPackageInstaller() ShizukuSystemServerApi.PackageManager_getPackageInstaller()
isRoot = Shizuku.getUid() == 0 val isRoot = Shizuku.getUid() == 0
// The reason for use "com.android.shell" as installer package under adb
// the reason for use "com.android.shell" as installer package under adb is that getMySessions will check installer package's owner // is that getMySessions will check installer package's owner
installerPackageName = if (isRoot) packageName else "com.android.shell" val installerPackageName = if (isRoot) packageName else "com.android.shell"
var installerAttributionTag: String? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installerAttributionTag = attributionTag installerAttributionTag = attributionTag
} }
userId = if (isRoot) Process.myUserHandle().hashCode() else 0 val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
packageInstaller = PackageInstallerUtils.createPackageInstaller( val packageInstaller = PackageInstallerUtils.createPackageInstaller(
_packageInstaller, iPackageInstaller, installerPackageName, installerAttributionTag, userId)
installerPackageName,
installerAttributionTag,
userId
)
val sessionId: Int
res.append("createSession: ")
val params = val params =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
var installFlags: Int = PackageInstallerUtils.getInstallFlags(params) var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
installFlags = installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
installFlags or (0x00000004 /*PackageManager.INSTALL_ALLOW_TEST*/ or 0x00000002) /*PackageManager.INSTALL_REPLACE_EXISTING*/
PackageInstallerUtils.setInstallFlags(params, installFlags) PackageInstallerUtils.setInstallFlags(params, installFlags)
sessionId = packageInstaller.createSession(params) val sessionId = packageInstaller.createSession(params)
res.append(sessionId).append('\n') val iSession = IPackageInstallerSession.Stub.asInterface(
res.append('\n').append("write: ") ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
val _session = IPackageInstallerSession.Stub.asInterface( session = PackageInstallerUtils.createSession(iSession)
ShizukuBinderWrapper( val inputStream = contentResolver.openInputStream(uri)
_packageInstaller.openSession(sessionId).asBinder() val openedSession = session.openWrite("apk.apk", 0, -1)
) val buffer = ByteArray(8192)
) var length: Int
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 { try {
while (`is`!!.read(buf).also { len = it } > 0) { while (inputStream!!.read(buffer).also { length = it } > 0) {
os.write(buf, 0, len) openedSession.write(buffer, 0, length)
os.flush() openedSession.flush()
session.fsync(os) session.fsync(openedSession)
} }
} finally { } finally {
try { try {
`is`!!.close() inputStream!!.close()
} catch (e: IOException) { openedSession.close()
e.printStackTrace()
}
try {
os.close()
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
} }
res.append('\n').append("commit: ")
val results = arrayOf<Intent?>(null) val results = arrayOf<Intent?>(null)
val countDownLatch = CountDownLatch(1) val countDownLatch = CountDownLatch(1)
val intentSender: IntentSender = val intentSender: IntentSender =
@ -139,66 +110,62 @@ class MainActivity: FlutterActivity() {
}) })
session.commit(intentSender) session.commit(intentSender)
countDownLatch.await() countDownLatch.await()
val result = results[0] res = results[0]!!.getIntExtra(
val status = PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
result!!.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) } catch (_: Exception) {
val message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) res = false
res.append('\n').append("status: ").append(status).append(" (").append(message)
.append(")")
} catch (tr: Throwable) {
tr.printStackTrace()
res.append(tr)
} finally { } finally {
if (session != null) { if (session != null) {
try { try {
session.close() session.close()
} catch (tr: Throwable) { } catch (_: Exception) {
res.append(tr) res = false
} }
} }
} }
result.success(res)
} }
private fun installWithShizuku(apkFilePath: String, result: Result) { private fun rootCheckPermission(result: Result) {
shizukuCheckPermission() Shell.getShell(Shell.GetShellCallback(
shizukuInstallApk(Uri.parse("file://$apkFilePath")) fun(shell: Shell) {
result.success(0) result.success(shell.isRoot)
}
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 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) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installersChannel).setMethodCallHandler { Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
installersChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "installers")
installersChannel!!.setMethodCallHandler {
call, result -> call, result ->
var apkFilePath: String? = call.argument("apkFilePath") if (call.method == "checkPermissionShizuku") {
if (call.method == "installWithShizuku") { shizukuCheckPermission()
installWithShizuku(apkFilePath.toString(), result) 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") { } 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Shizuku.removeBinderReceivedListener(shizukuBinderReceivedListener)
Shizuku.removeBinderDeadListener(shizukuBinderDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener) Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
} }
} }

View File

@ -282,6 +282,7 @@
"normal": "Normal", "normal": "Normal",
"shizuku": "Shizuku", "shizuku": "Shizuku",
"root": "Root", "root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"

View File

@ -275,13 +275,14 @@
"downloadingXNotifChannel": "Загрузка {}", "downloadingXNotifChannel": "Загрузка {}",
"completeAppInstallationNotifChannel": "Завершение установки приложения", "completeAppInstallationNotifChannel": "Завершение установки приложения",
"checkingForUpdatesNotifChannel": "Проверка обновлений", "checkingForUpdatesNotifChannel": "Проверка обновлений",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
"supportFixedAPKURL": "Support fixed APK URLs", "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
"selectX": "Select {}", "selectX": "Выбрать {}",
"installMethod": "Метод установки", "installMethod": "Метод установки",
"normal": "Нормальный", "normal": "Нормальный",
"shizuku": "Shizuku", "shizuku": "Shizuku",
"root": "Суперпользователь", "root": "Суперпользователь",
"shizukuBinderNotFound": "Shizuku не запущен",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Удалить приложение?", "one": "Удалить приложение?",
"other": "Удалить приложения?" "other": "Удалить приложения?"

View File

@ -505,7 +505,8 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) { !(await canDowngradeApps())) {
throw DowngradeError(); throw DowngradeError();
} }
if (needsBGWorkaround) { if (needsBGWorkaround &&
settingsProvider.installMethod == InstallMethodSettings.normal) {
// The below 'await' will never return if we are in a background process // 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 // 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 // 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); attemptToCorrectInstallStatus: false);
} }
int? code; int? code;
if (settingsProvider.installMethod == InstallMethodSettings.normal) { switch (settingsProvider.installMethod) {
code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); case InstallMethodSettings.normal:
} else if (settingsProvider.installMethod == InstallMethodSettings.shizuku) { code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
code = await Installers.installWithShizuku(apkFilePath: file.file.path); case InstallMethodSettings.shizuku:
} else if (settingsProvider.installMethod == InstallMethodSettings.root) { code = (await Installers.installWithShizuku(apkFileUri: file.file.uri.toString())) ? 0 : 1;
code = await Installers.installWithRoot(apkFilePath: file.file.path); case InstallMethodSettings.root:
code = (await Installers.installWithRoot(apkFilePath: file.file.path)) ? 0 : 1;
} }
bool installed = false; bool installed = false;
if (code != null && code != 0 && code != 3) { if (code != null && code != 0 && code != 3) {
@ -679,8 +681,22 @@ class AppsProvider with ChangeNotifier {
} }
var appId = downloadedFile?.appId ?? downloadedDir!.appId; var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent = await canInstallSilently(apps[appId]!.app); bool willBeSilent = await canInstallSilently(apps[appId]!.app);
if (!(await settingsProvider.getInstallPermission(enforce: false))) { switch (settingsProvider.installMethod) {
throw ObtainiumError(tr('cancelled')); 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) { if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously

View File

@ -3,12 +3,52 @@ import 'package:flutter/services.dart';
class Installers { class Installers {
static const MethodChannel _channel = MethodChannel('installers'); static const MethodChannel _channel = MethodChannel('installers');
static bool _callbacksApplied = false;
static int _resPermShizuku = -2; // not set
static Future<int?> installWithShizuku({required String apkFilePath}) async { static Future waitWhile(bool Function() test,
return await _channel.invokeMethod('installWithShizuku', {'apkFilePath': apkFilePath}); [Duration pollInterval = const Duration(milliseconds: 100)]) {
var completer = Completer();
check() {
if (test()) {
Timer(pollInterval, check);
} else {
completer.complete();
}
}
check();
return completer.future;
} }
static Future<int?> installWithRoot({required String apkFilePath}) async { static Future handleCalls(MethodCall call) async {
return await _channel.invokeMethod('installWithRoot', {'apkFilePath': apkFilePath}); if (call.method == 'resPermShizuku') {
_resPermShizuku = call.arguments['res'];
}
}
static Future<int> 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<bool> checkPermissionRoot() async {
return await _channel.invokeMethod('checkPermissionRoot');
}
static Future<bool> installWithShizuku({required String apkFileUri}) async {
return await _channel.invokeMethod(
'installWithShizuku', {'apkFileUri': apkFileUri});
}
static Future<bool> installWithRoot({required String apkFilePath}) async {
return await _channel.invokeMethod(
'installWithRoot', {'apkFilePath': apkFilePath});
} }
} }