mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v0.14.40-b
			...
			v0.14.41-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					45fa0a165a | ||
| 
						 | 
					0e5c07a078 | ||
| 
						 | 
					601a742c71 | ||
| 
						 | 
					c972401b6e | ||
| 
						 | 
					024e81cf01 | ||
| 
						 | 
					975ed402d5 | ||
| 
						 | 
					b9e8083744 | ||
| 
						 | 
					bb859708bc | ||
| 
						 | 
					3cf2c221ac | ||
| 
						 | 
					6edd7edcd2 | ||
| 
						 | 
					4e26a02d78 | ||
| 
						 | 
					bb36a57053 | ||
| 
						 | 
					b291c800f1 | ||
| 
						 | 
					375b9bce30 | ||
| 
						 | 
					b6b8db48df | ||
| 
						 | 
					36e6c267b9 | ||
| 
						 | 
					de60c4ee9e | ||
| 
						 | 
					de67e40c00 | 
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
@@ -90,6 +91,24 @@ flutter {
 | 
			
		||||
    source '../..'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    maven { url 'https://jitpack.io' }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    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:resource="@xml/file_paths" />
 | 
			
		||||
        </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>
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
    <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 {
 | 
			
		||||
        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!
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -281,6 +281,11 @@
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remove App?",
 | 
			
		||||
        "other": "Remove Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,9 @@
 | 
			
		||||
    "unexpectedError": "Error Inesperado",
 | 
			
		||||
    "ok": "Correcto",
 | 
			
		||||
    "and": "y",
 | 
			
		||||
    "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
 | 
			
		||||
    "githubPATLabel": "Token Github de Acceso Personal\n(Reduce tiempos de espera)",
 | 
			
		||||
    "includePrereleases": "Incluir versiones preliminares",
 | 
			
		||||
    "fallbackToOlderReleases": "Retorceder a versiones previas",
 | 
			
		||||
    "fallbackToOlderReleases": "Retroceder a versiones previas",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
 | 
			
		||||
    "invalidRegEx": "Expresión inválida",
 | 
			
		||||
    "noDescription": "Sin descripción",
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
    "requiredInBrackets": "(Requerido)",
 | 
			
		||||
    "dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
 | 
			
		||||
    "colour": "Color",
 | 
			
		||||
    "githubStarredRepos": "Repositorios favoritos de GitHub",
 | 
			
		||||
    "githubStarredRepos": "Repositorios favoritos GitHub",
 | 
			
		||||
    "uname": "Nombre de usuario",
 | 
			
		||||
    "wrongArgNum": "Número de argumentos provistos inválido",
 | 
			
		||||
    "xIsTrackOnly": "{} es de 'Solo Seguimiento'",
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
    "app": "Aplicación",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
 | 
			
		||||
    "youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
 | 
			
		||||
    "trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o actalizarla.",
 | 
			
		||||
    "trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
 | 
			
		||||
    "cancelled": "Cancelado",
 | 
			
		||||
    "appAlreadyAdded": "Aplicación ya añadida",
 | 
			
		||||
    "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
 | 
			
		||||
@@ -38,12 +38,12 @@
 | 
			
		||||
    "appSourceURL": "URL de Origen de la Aplicación",
 | 
			
		||||
    "error": "Error",
 | 
			
		||||
    "add": "Añadir",
 | 
			
		||||
    "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
 | 
			
		||||
    "searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
 | 
			
		||||
    "search": "Buscar",
 | 
			
		||||
    "additionalOptsFor": "Opciones Adicionales para {}",
 | 
			
		||||
    "supportedSources": "Fuentes Soportadas",
 | 
			
		||||
    "trackOnlyInBrackets": "(Solo Seguimiento)",
 | 
			
		||||
    "searchableInBrackets": "(Soporta Búsquedas)",
 | 
			
		||||
    "searchableInBrackets": "(soporta búsqueda)",
 | 
			
		||||
    "appsString": "Aplicaciones",
 | 
			
		||||
    "noApps": "Sin Aplicaciones",
 | 
			
		||||
    "noAppsForFilter": "Sin Aplicaciones para Filtrar",
 | 
			
		||||
@@ -56,9 +56,9 @@
 | 
			
		||||
    "estimateInBrackets": "(Aproximado)",
 | 
			
		||||
    "selectAll": "Seleccionar Todo",
 | 
			
		||||
    "deselectX": "Deseleccionar {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
 | 
			
		||||
    "removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
 | 
			
		||||
    "removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
 | 
			
		||||
    "updateX": "Actualizar {}",
 | 
			
		||||
    "installX": "Instalar {}",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
 | 
			
		||||
@@ -98,8 +98,8 @@
 | 
			
		||||
    "line": "Línea",
 | 
			
		||||
    "searchX": "Buscar {}",
 | 
			
		||||
    "noResults": "Resultados no encontrados",
 | 
			
		||||
    "importX": "Importar {}",
 | 
			
		||||
    "importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
 | 
			
		||||
    "importX": "Importar desde {}",
 | 
			
		||||
    "importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
 | 
			
		||||
    "importErrors": "Errores de Importación",
 | 
			
		||||
    "importedXOfYApps": "{} de {} Aplicaciones importadas.",
 | 
			
		||||
    "followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
 | 
			
		||||
@@ -113,12 +113,12 @@
 | 
			
		||||
    "followSystem": "Seguir al Sistema",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Usar tema oscuro con negros puros",
 | 
			
		||||
    "appSortBy": "Ordenar Aplicaciones Por",
 | 
			
		||||
    "useBlackTheme": "Usar negros puros en tema oscuro",
 | 
			
		||||
    "appSortBy": "Ordenar Apps Por",
 | 
			
		||||
    "authorName": "Autor/Nombre",
 | 
			
		||||
    "nameAuthor": "Nombre/Autor",
 | 
			
		||||
    "asAdded": "Según se Añadieron",
 | 
			
		||||
    "appSortOrder": "Orden de Clasificación de Aplicaciones",
 | 
			
		||||
    "appSortOrder": "Orden de Clasificación",
 | 
			
		||||
    "ascending": "Ascendente",
 | 
			
		||||
    "descending": "Descendente",
 | 
			
		||||
    "bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
 | 
			
		||||
@@ -170,12 +170,12 @@
 | 
			
		||||
    "lastUpdateCheckX": "Última Comprobación: {}",
 | 
			
		||||
    "remove": "Eliminar",
 | 
			
		||||
    "yesMarkUpdated": "Sí, Marcar como Actualizada",
 | 
			
		||||
    "fdroid": "Repositorio oficial de F-Droid",
 | 
			
		||||
    "fdroid": "Repositorio oficial F-Droid",
 | 
			
		||||
    "appIdOrName": "ID o Nombre de la Aplicación",
 | 
			
		||||
    "appId": "ID de la Aplicación",
 | 
			
		||||
    "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
 | 
			
		||||
    "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
 | 
			
		||||
    "fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
 | 
			
		||||
    "fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
 | 
			
		||||
    "steam": "Steam",
 | 
			
		||||
    "steamMobile": "Steam Mobile",
 | 
			
		||||
    "steamChat": "Steam Chat",
 | 
			
		||||
@@ -195,8 +195,8 @@
 | 
			
		||||
    "category": "Categoría",
 | 
			
		||||
    "noCategory": "Sin Categoría",
 | 
			
		||||
    "noCategories": "Sin Categorías",
 | 
			
		||||
    "deleteCategoriesQuestion": "¿Borrar Categorías?",
 | 
			
		||||
    "categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán marcadas como 'Sin Categoría'.",
 | 
			
		||||
    "deleteCategoriesQuestion": "¿Eliminar Categorías?",
 | 
			
		||||
    "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
 | 
			
		||||
    "addCategory": "Añadir Categoría",
 | 
			
		||||
    "label": "Nombre",
 | 
			
		||||
    "language": "Idioma",
 | 
			
		||||
@@ -211,7 +211,7 @@
 | 
			
		||||
    "releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
 | 
			
		||||
    "changes": "Cambios",
 | 
			
		||||
    "releaseDate": "Fecha de Publicación",
 | 
			
		||||
    "importFromURLsInFile": "Importar de URls desde un Archivo (como OPML)",
 | 
			
		||||
    "importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
 | 
			
		||||
    "versionDetection": "Detección de Versiones",
 | 
			
		||||
    "standardVersionDetection": "Detección de versiones estándar",
 | 
			
		||||
    "groupByCategory": "Agrupar por Categoría",
 | 
			
		||||
@@ -219,8 +219,8 @@
 | 
			
		||||
    "overrideSource": "Sobrescribir Fuente",
 | 
			
		||||
    "dontShowAgain": "No mostrar de nuevo",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover las Apps no instaladas al final de la vista de Apps",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APKs",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover Apps no instaladas en la Parte Inferior de la Vista de Aplicaciones",
 | 
			
		||||
    "gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
 | 
			
		||||
    "about": "Acerca",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
 | 
			
		||||
@@ -230,7 +230,7 @@
 | 
			
		||||
    "pickHighestVersionCode": "Auto selección versión superior del código APK",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
 | 
			
		||||
    "disablePageTransitions": "Deshabilitar animaciones de transición de la página",
 | 
			
		||||
    "reversePageTransitions": "Invertir las animaciones de transición de la página",
 | 
			
		||||
    "reversePageTransitions": "Invertir animaciones de transición de la página",
 | 
			
		||||
    "minStarCount": "Número Mínimo de Estrellas",
 | 
			
		||||
    "addInfoBelow": "Añadir esta información debajo.",
 | 
			
		||||
    "addInfoInSettings": "Añadir esta información en Ajustes.",
 | 
			
		||||
@@ -244,25 +244,25 @@
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede verificar con Obtainium abierto.",
 | 
			
		||||
    "verifyLatestTag": "Verifica la etiqueta 'latest'",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
 | 
			
		||||
    "verifyLatestTag": "Comprueba la etiqueta 'latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
 | 
			
		||||
    "intermediateLinkNotFound": "Enlace Intermedio no encontrado",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
 | 
			
		||||
    "versionExtractionRegEx": "Versión de Extracción de RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "matchGroupToUse": "Coincidir en Grupo a Usar",
 | 
			
		||||
    "highlightTouchTargets": "Resaltar objetivos menos obvios",
 | 
			
		||||
    "pickExportDir": "Selecciona el Directorio para Exportar",
 | 
			
		||||
    "autoExportOnChanges": "Auto Exportar cuando haya cambios",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "includeSettings": "Incluir ajustes",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrar por Versiones",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
 | 
			
		||||
    "dontSortReleasesList": "Mantener el order de publicación desde API",
 | 
			
		||||
    "reverseSort": "Orden inverso",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "takeFirstLink": "Usar primer enlace",
 | 
			
		||||
    "skipSort": "Omitir orden",
 | 
			
		||||
    "debugMenu": "Menu Depurar",
 | 
			
		||||
    "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
 | 
			
		||||
    "runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
 | 
			
		||||
@@ -277,10 +277,10 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Descargando {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas y en seguimiento",
 | 
			
		||||
    "supportFixedAPKURL": "Soporte para URLs fijas de APK",
 | 
			
		||||
    "selectX": "Selecciona {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "parallelDownloads": "Permitir descargas paralelas",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "¿Eliminar Aplicación?",
 | 
			
		||||
        "other": "¿Eliminar Aplicaciones?"
 | 
			
		||||
@@ -318,8 +318,8 @@
 | 
			
		||||
        "other": "{} Días"
 | 
			
		||||
    },
 | 
			
		||||
    "clearedNLogsBeforeXAfterY": {
 | 
			
		||||
        "one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
 | 
			
		||||
        "other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
			
		||||
        "one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
 | 
			
		||||
        "other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesAvailable": {
 | 
			
		||||
        "one": "{} y 1 aplicación más tiene actualizaciones.",
 | 
			
		||||
 
 | 
			
		||||
@@ -277,10 +277,15 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Загрузка {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Завершение установки приложения",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Проверка обновлений",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
 | 
			
		||||
    "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
 | 
			
		||||
    "selectX": "Выбрать {}",
 | 
			
		||||
    "parallelDownloads": "Разрешить параллельные загрузки",
 | 
			
		||||
    "installMethod": "Метод установки",
 | 
			
		||||
    "normal": "Нормальный",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Суперпользователь",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku не запущен",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Удалить приложение?",
 | 
			
		||||
        "other": "Удалить приложения?"
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ class HTML extends AppSource {
 | 
			
		||||
            label: tr('sortByFileNamesNotLinks'))
 | 
			
		||||
      ],
 | 
			
		||||
      [GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
 | 
			
		||||
      [GeneratedFormSwitch('reverseSort', label: tr('takeTopLink'))],
 | 
			
		||||
      [GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSwitch('supportFixedAPKURL',
 | 
			
		||||
            defaultValue: true, label: tr('supportFixedAPKURL')),
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.14.40';
 | 
			
		||||
const String currentVersion = '0.14.41';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
      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,
 | 
			
		||||
@@ -328,6 +351,8 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            height16,
 | 
			
		||||
                            installMethodDropdown,
 | 
			
		||||
                            height16,
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                              children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
@@ -504,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
 | 
			
		||||
@@ -515,8 +517,21 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      await saveApps([apps[file.appId]!.app],
 | 
			
		||||
          attemptToCorrectInstallStatus: false);
 | 
			
		||||
    }
 | 
			
		||||
    int? code =
 | 
			
		||||
        await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
 | 
			
		||||
    int? code;
 | 
			
		||||
    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) {
 | 
			
		||||
      throw InstallError(code);
 | 
			
		||||
@@ -672,8 +687,23 @@ 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
 | 
			
		||||
@@ -720,7 +750,9 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      await Future.wait(
 | 
			
		||||
          appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
 | 
			
		||||
      for (var id in appsToInstall) {
 | 
			
		||||
        await updateFn(id);
 | 
			
		||||
        if (!errors.appIdNames.containsKey(id)) {
 | 
			
		||||
          await updateFn(id);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 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];
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 0.14.40+234 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.14.41+235 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=3.0.0 <4.0.0'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user