mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 13:33:28 +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,9 +687,24 @@ class AppsProvider with ChangeNotifier { | ||||
|         } | ||||
|         var appId = downloadedFile?.appId ?? downloadedDir!.appId; | ||||
|         bool willBeSilent = await canInstallSilently(apps[appId]!.app); | ||||
|         if (!(await settingsProvider.getInstallPermission(enforce: false))) { | ||||
|         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 | ||||
|           await waitForUserToReturnToForeground(context); | ||||
| @@ -720,9 +750,11 @@ class AppsProvider with ChangeNotifier { | ||||
|       await Future.wait( | ||||
|           appsToInstall.map((id) => updateFn(id, skipInstalls: true))); | ||||
|       for (var id in appsToInstall) { | ||||
|         if (!errors.appIdNames.containsKey(id)) { | ||||
|           await updateFn(id); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (errors.idsByErrorString.isNotEmpty) { | ||||
|       throw errors; | ||||
|   | ||||
							
								
								
									
										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