mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-19 13:09:30 +02:00
Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
|
|||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'dev.rikka.tools.refine'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
@@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -52,8 +53,8 @@ android {
|
|||||||
applicationId "dev.imranr.obtainium"
|
applicationId "dev.imranr.obtainium"
|
||||||
// You can update the following values to match your application needs.
|
// 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.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 23
|
minSdkVersion 24
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@@ -90,6 +91,24 @@ flutter {
|
|||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
|
def shizuku_version = '13.1.5'
|
||||||
|
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
||||||
|
implementation "dev.rikka.shizuku:provider:$shizuku_version"
|
||||||
|
|
||||||
|
def hidden_api_version = '4.1.0'
|
||||||
|
// DO NOT UPDATE Hidden API without updating the Android tools
|
||||||
|
// and do not update Android tools without updating the whole Flutter
|
||||||
|
// (also in android/build.gradle)
|
||||||
|
implementation "dev.rikka.tools.refine:runtime:$hidden_api_version"
|
||||||
|
implementation "dev.rikka.hidden:compat:$hidden_api_version"
|
||||||
|
compileOnly "dev.rikka.hidden:stub:$hidden_api_version"
|
||||||
|
|
||||||
|
implementation "com.github.topjohnwu.libsu:core:5.2.2"
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,13 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
package dev.imranr.obtainium
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
}
|
|
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package dev.imranr.obtainium
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.content.pm.IPackageInstaller
|
||||||
|
import android.content.pm.IPackageInstallerSession
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Process
|
||||||
|
import androidx.annotation.NonNull
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import dev.imranr.obtainium.util.IIntentSenderAdaptor
|
||||||
|
import dev.imranr.obtainium.util.IntentSenderUtils
|
||||||
|
import dev.imranr.obtainium.util.PackageInstallerUtils
|
||||||
|
import dev.imranr.obtainium.util.ShizukuSystemServerApi
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
|
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
|
||||||
|
import rikka.shizuku.ShizukuBinderWrapper
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity() {
|
||||||
|
private var installersChannel: MethodChannel? = null
|
||||||
|
private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
|
||||||
|
|
||||||
|
private fun shizukuCheckPermission(result: Result) {
|
||||||
|
try {
|
||||||
|
if (Shizuku.isPreV11()) { // Unsupported
|
||||||
|
result.success(-1)
|
||||||
|
} else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
result.success(1)
|
||||||
|
} else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
|
||||||
|
result.success(0)
|
||||||
|
} else {
|
||||||
|
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||||
|
result.success(-2)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { // If shizuku not running
|
||||||
|
result.success(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
|
||||||
|
requestCode: Int, grantResult: Int ->
|
||||||
|
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||||
|
val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
|
||||||
|
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shizukuInstallApk(apkFileUri: String, result: Result) {
|
||||||
|
val uri = Uri.parse(apkFileUri)
|
||||||
|
var res = false
|
||||||
|
var session: PackageInstaller.Session? = null
|
||||||
|
try {
|
||||||
|
val iPackageInstaller: IPackageInstaller =
|
||||||
|
ShizukuSystemServerApi.PackageManager_getPackageInstaller()
|
||||||
|
val isRoot = Shizuku.getUid() == 0
|
||||||
|
// The reason for use "com.android.shell" as installer package under adb
|
||||||
|
// is that getMySessions will check installer package's owner
|
||||||
|
val installerPackageName = if (isRoot) packageName else "com.android.shell"
|
||||||
|
var installerAttributionTag: String? = null
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
installerAttributionTag = attributionTag
|
||||||
|
}
|
||||||
|
val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
|
||||||
|
val packageInstaller = PackageInstallerUtils.createPackageInstaller(
|
||||||
|
iPackageInstaller, installerPackageName, installerAttributionTag, userId)
|
||||||
|
val params =
|
||||||
|
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
|
||||||
|
installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
|
||||||
|
PackageInstallerUtils.setInstallFlags(params, installFlags)
|
||||||
|
val sessionId = packageInstaller.createSession(params)
|
||||||
|
val iSession = IPackageInstallerSession.Stub.asInterface(
|
||||||
|
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
|
||||||
|
session = PackageInstallerUtils.createSession(iSession)
|
||||||
|
val inputStream = contentResolver.openInputStream(uri)
|
||||||
|
val openedSession = session.openWrite("apk.apk", 0, -1)
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var length: Int
|
||||||
|
try {
|
||||||
|
while (inputStream!!.read(buffer).also { length = it } > 0) {
|
||||||
|
openedSession.write(buffer, 0, length)
|
||||||
|
openedSession.flush()
|
||||||
|
session.fsync(openedSession)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
inputStream!!.close()
|
||||||
|
openedSession.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val results = arrayOf<Intent?>(null)
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
val intentSender: IntentSender =
|
||||||
|
IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() {
|
||||||
|
override fun send(intent: Intent?) {
|
||||||
|
results[0] = intent
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
session.commit(intentSender)
|
||||||
|
countDownLatch.await()
|
||||||
|
res = results[0]!!.getIntExtra(
|
||||||
|
PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
res = false
|
||||||
|
} finally {
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
session.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
res = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootCheckPermission(result: Result) {
|
||||||
|
Shell.getShell(Shell.GetShellCallback(
|
||||||
|
fun(shell: Shell) {
|
||||||
|
result.success(shell.isRoot)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootInstallApk(apkFilePath: String, result: Result) {
|
||||||
|
Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
|
||||||
|
val builder = StringBuilder()
|
||||||
|
for (data in out.getOut()) { builder.append(data) }
|
||||||
|
result.success(builder.toString().endsWith("Success"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||||
|
installersChannel = MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger, "installers")
|
||||||
|
installersChannel!!.setMethodCallHandler {
|
||||||
|
call, result ->
|
||||||
|
if (call.method == "checkPermissionShizuku") {
|
||||||
|
shizukuCheckPermission(result)
|
||||||
|
} else if (call.method == "checkPermissionRoot") {
|
||||||
|
rootCheckPermission(result)
|
||||||
|
} else if (call.method == "installWithShizuku") {
|
||||||
|
val apkFileUri: String? = call.argument("apkFileUri")
|
||||||
|
shizukuInstallApk(apkFileUri!!, result)
|
||||||
|
} else if (call.method == "installWithRoot") {
|
||||||
|
val apkFilePath: String? = call.argument("apkFilePath")
|
||||||
|
rootInstallApk(apkFilePath!!, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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<IPackageManager> PACKAGE_MANAGER = new Singleton<IPackageManager>() {
|
||||||
|
@Override
|
||||||
|
protected IPackageManager create() {
|
||||||
|
return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Singleton<IUserManager> USER_MANAGER = new Singleton<IUserManager>() {
|
||||||
|
@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<UserInfo> 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<UserInfo> 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<UserInfo> 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;
|
||||||
|
}*/
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.imranr.obtainium.util;
|
||||||
|
|
||||||
|
public abstract class Singleton<T> {
|
||||||
|
|
||||||
|
private T mInstance;
|
||||||
|
|
||||||
|
protected abstract T create();
|
||||||
|
|
||||||
|
public final T get() {
|
||||||
|
synchronized (this) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = create();
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath 'dev.rikka.tools.refine:gradle-plugin:4.1.0' // Do not update!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -281,6 +281,11 @@
|
|||||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||||
"selectX": "Select {}",
|
"selectX": "Select {}",
|
||||||
"parallelDownloads": "Allow parallel downloads",
|
"parallelDownloads": "Allow parallel downloads",
|
||||||
|
"installMethod": "Installation method",
|
||||||
|
"normal": "Normal",
|
||||||
|
"shizuku": "Shizuku",
|
||||||
|
"root": "Root",
|
||||||
|
"shizukuBinderNotFound": "Shizuku is not running",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
|
@@ -277,10 +277,15 @@
|
|||||||
"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": "Выбрать {}",
|
||||||
"parallelDownloads": "Allow parallel downloads",
|
"parallelDownloads": "Разрешить параллельные загрузки",
|
||||||
|
"installMethod": "Метод установки",
|
||||||
|
"normal": "Нормальный",
|
||||||
|
"shizuku": "Shizuku",
|
||||||
|
"root": "Суперпользователь",
|
||||||
|
"shizukuBinderNotFound": "Shizuku не запущен",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Удалить приложение?",
|
"one": "Удалить приложение?",
|
||||||
"other": "Удалить приложения?"
|
"other": "Удалить приложения?"
|
||||||
|
@@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
settingsProvider.initializeSettings();
|
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(
|
var themeDropdown = DropdownButtonFormField(
|
||||||
decoration: InputDecoration(labelText: tr('theme')),
|
decoration: InputDecoration(labelText: tr('theme')),
|
||||||
value: settingsProvider.theme,
|
value: settingsProvider.theme,
|
||||||
@@ -328,6 +351,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
|
installMethodDropdown,
|
||||||
|
height16,
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
@@ -33,6 +33,7 @@ import 'package:http/http.dart';
|
|||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:flutter_archive/flutter_archive.dart';
|
import 'package:flutter_archive/flutter_archive.dart';
|
||||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||||
|
import 'installers_provider.dart';
|
||||||
|
|
||||||
final pm = AndroidPackageManager();
|
final pm = AndroidPackageManager();
|
||||||
|
|
||||||
@@ -504,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
|
||||||
@@ -515,8 +517,15 @@ class AppsProvider with ChangeNotifier {
|
|||||||
await saveApps([apps[file.appId]!.app],
|
await saveApps([apps[file.appId]!.app],
|
||||||
attemptToCorrectInstallStatus: false);
|
attemptToCorrectInstallStatus: false);
|
||||||
}
|
}
|
||||||
int? code =
|
int? code;
|
||||||
await AndroidPackageInstaller.installApk(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;
|
bool installed = false;
|
||||||
if (code != null && code != 0 && code != 3) {
|
if (code != null && code != 0 && code != 3) {
|
||||||
throw InstallError(code);
|
throw InstallError(code);
|
||||||
@@ -672,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);
|
||||||
|
56
lib/providers/installers_provider.dart
Normal file
56
lib/providers/installers_provider.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class Installers {
|
||||||
|
static const MethodChannel _channel = MethodChannel('installers');
|
||||||
|
static bool _callbacksApplied = false;
|
||||||
|
static int _resPermShizuku = -2; // not set
|
||||||
|
|
||||||
|
static Future waitWhile(bool Function() test,
|
||||||
|
[Duration pollInterval = const Duration(milliseconds: 250)]) {
|
||||||
|
var completer = Completer();
|
||||||
|
check() {
|
||||||
|
if (test()) {
|
||||||
|
Timer(pollInterval, check);
|
||||||
|
} else {
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future handleCalls(MethodCall call) async {
|
||||||
|
if (call.method == 'resPermShizuku') {
|
||||||
|
_resPermShizuku = call.arguments['res'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<int> checkPermissionShizuku() async {
|
||||||
|
if (!_callbacksApplied) {
|
||||||
|
_channel.setMethodCallHandler(handleCalls);
|
||||||
|
_callbacksApplied = true;
|
||||||
|
}
|
||||||
|
int res = await _channel.invokeMethod('checkPermissionShizuku');
|
||||||
|
if(res == -2) {
|
||||||
|
await waitWhile(() => _resPermShizuku == -2);
|
||||||
|
res = _resPermShizuku;
|
||||||
|
_resPermShizuku = -2;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<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});
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
|
|||||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||||
String obtainiumId = 'dev.imranr.obtainium';
|
String obtainiumId = 'dev.imranr.obtainium';
|
||||||
|
|
||||||
|
enum InstallMethodSettings { normal, shizuku, root }
|
||||||
|
|
||||||
enum ThemeSettings { system, light, dark }
|
enum ThemeSettings { system, light, dark }
|
||||||
|
|
||||||
enum ColourSettings { basic, materialYou }
|
enum ColourSettings { basic, materialYou }
|
||||||
@@ -49,6 +51,16 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
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 {
|
ThemeSettings get theme {
|
||||||
return ThemeSettings
|
return ThemeSettings
|
||||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
||||||
|
Reference in New Issue
Block a user