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 {
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() { private fun shizukuCheckPermission() {
if(Shizuku.isPreV11()) { try {
shizukuPermissionGranted = false if (Shizuku.isPreV11()) { // Unsupported
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to -1))
} else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { } else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
shizukuPermissionGranted = true installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to 1))
} else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again } else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
shizukuPermissionGranted = false installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to 0))
} else { } else {
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE) Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
} }
} catch (_: Exception) { // If shizuku not running
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to -1))
}
} }
private fun shizukuInstallApk(uri: Uri) { private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
val packageInstaller: PackageInstaller requestCode: Int, grantResult: Int ->
var session: PackageInstaller.Session? = null if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
val cr = contentResolver val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
val res = StringBuilder() installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
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 private fun shizukuInstallApk(apkFileUri: String, result: Result) {
installerPackageName = if (isRoot) packageName else "com.android.shell" 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) { 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) { private fun rootInstallApk(apkFilePath: String, result: Result) {
Shell.sh("pm install -r -t " + apkFilePath).submit { out -> Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
val builder = StringBuilder() val builder = StringBuilder()
for (data in out.getOut()) { for (data in out.getOut()) { builder.append(data) }
builder.append(data) result.success(builder.toString().endsWith("Success"))
}
result.success(if (builder.toString().endsWith("Success")) 0 else 1)
} }
} }
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(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)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Shizuku.addBinderReceivedListener(shizukuBinderReceivedListener)
Shizuku.addBinderDeadListener(shizukuBinderDeadListener)
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener) Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
installersChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "installers")
installersChannel!!.setMethodCallHandler {
call, 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") {
val apkFilePath: String? = call.argument("apkFilePath")
rootInstallApk(apkFilePath!!, result)
}
}
} }
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) {
case InstallMethodSettings.normal:
code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
} else if (settingsProvider.installMethod == InstallMethodSettings.shizuku) { case InstallMethodSettings.shizuku:
code = await Installers.installWithShizuku(apkFilePath: file.file.path); code = (await Installers.installWithShizuku(apkFileUri: file.file.uri.toString())) ? 0 : 1;
} else if (settingsProvider.installMethod == InstallMethodSettings.root) { case InstallMethodSettings.root:
code = await Installers.installWithRoot(apkFilePath: file.file.path); 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,9 +681,23 @@ 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);
switch (settingsProvider.installMethod) {
case InstallMethodSettings.normal:
if (!(await settingsProvider.getInstallPermission(enforce: false))) { if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled')); 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
await waitForUserToReturnToForeground(context); await waitForUserToReturnToForeground(context);

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});
} }
} }