mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-18 04:38:10 +02:00
2
.flutter
2
.flutter
Submodule .flutter updated: 300451adae...41456452f2
@@ -92,20 +92,6 @@ repositories {
|
|||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
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.3.1'
|
|
||||||
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 "org.lsposed.hiddenapibypass:hiddenapibypass:4.3"
|
|
||||||
|
|
||||||
implementation "com.github.topjohnwu.libsu:core:5.2.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
|
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
|
||||||
import com.android.build.OutputFile
|
import com.android.build.OutputFile
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
|
@@ -1,44 +0,0 @@
|
|||||||
package dev.imranr.obtainium
|
|
||||||
|
|
||||||
import android.util.Xml
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
|
|
||||||
class DefaultSystemFont {
|
|
||||||
fun get(): String {
|
|
||||||
return try {
|
|
||||||
val file = File("/system/etc/fonts.xml")
|
|
||||||
val fileStream = FileInputStream(file)
|
|
||||||
parseFontsFileStream(fileStream)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.message ?: "Unknown fonts.xml parsing exception"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseFontsFileStream(fileStream: FileInputStream): String {
|
|
||||||
fileStream.use { stream ->
|
|
||||||
val parser = Xml.newPullParser()
|
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
|
||||||
parser.setInput(stream, null)
|
|
||||||
parser.nextTag()
|
|
||||||
return parseFonts(parser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseFonts(parser: XmlPullParser): String {
|
|
||||||
while (!((parser.next() == XmlPullParser.END_TAG) && (parser.name == "family"))) {
|
|
||||||
if ((parser.eventType == XmlPullParser.START_TAG) && (parser.name == "font")
|
|
||||||
&& (parser.getAttributeValue(null, "style") == "normal")
|
|
||||||
&& (parser.getAttributeValue(null, "weight") == "400")) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parser.next()
|
|
||||||
val fontFile = parser.text.trim()
|
|
||||||
if (fontFile == "") {
|
|
||||||
throw NoSuchFieldException("The font filename couldn't be found in fonts.xml")
|
|
||||||
}
|
|
||||||
return "/system/fonts/$fontFile"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,179 +1,5 @@
|
|||||||
package dev.imranr.obtainium
|
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.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 org.lsposed.hiddenapibypass.HiddenApiBypass
|
|
||||||
import rikka.shizuku.Shizuku
|
|
||||||
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
|
|
||||||
import rikka.shizuku.ShizukuBinderWrapper
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity()
|
||||||
private var nativeChannel: 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
|
|
||||||
nativeChannel!!.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 (0x00000002/*PackageManager.INSTALL_REPLACE_EXISTING*/
|
|
||||||
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)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
HiddenApiBypass.addHiddenApiExemptions("")
|
|
||||||
}
|
|
||||||
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
|
||||||
nativeChannel = MethodChannel(
|
|
||||||
flutterEngine.dartExecutor.binaryMessenger, "native")
|
|
||||||
nativeChannel!!.setMethodCallHandler {
|
|
||||||
call, result ->
|
|
||||||
if (call.method == "getSystemFont") {
|
|
||||||
val res = DefaultSystemFont().get()
|
|
||||||
result.success(res)
|
|
||||||
} else 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,23 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
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;
|
|
||||||
}*/
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
@@ -22,6 +22,9 @@
|
|||||||
"requiredInBrackets": "(Required)",
|
"requiredInBrackets": "(Required)",
|
||||||
"dropdownNoOptsError": "ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT",
|
"dropdownNoOptsError": "ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT",
|
||||||
"colour": "Colour",
|
"colour": "Colour",
|
||||||
|
"standard": "Standard",
|
||||||
|
"custom": "Custom",
|
||||||
|
"useMaterialYou": "Use Material You",
|
||||||
"githubStarredRepos": "GitHub Starred Repos",
|
"githubStarredRepos": "GitHub Starred Repos",
|
||||||
"uname": "Username",
|
"uname": "Username",
|
||||||
"wrongArgNum": "Wrong number of arguments provided",
|
"wrongArgNum": "Wrong number of arguments provided",
|
||||||
@@ -143,8 +146,10 @@
|
|||||||
"noNewUpdates": "No new updates.",
|
"noNewUpdates": "No new updates.",
|
||||||
"xHasAnUpdate": "{} has an update.",
|
"xHasAnUpdate": "{} has an update.",
|
||||||
"appsUpdated": "Apps Updated",
|
"appsUpdated": "Apps Updated",
|
||||||
|
"appsNotUpdated": "Failed to update applications",
|
||||||
"appsUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were applied in the background",
|
"appsUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were applied in the background",
|
||||||
"xWasUpdatedToY": "{} was updated to {}.",
|
"xWasUpdatedToY": "{} was updated to {}.",
|
||||||
|
"xWasNotUpdatedToY": "Failed to update {} to {}.",
|
||||||
"errorCheckingUpdates": "Error Checking for Updates",
|
"errorCheckingUpdates": "Error Checking for Updates",
|
||||||
"errorCheckingUpdatesNotifDescription": "A notification that shows when background update checking fails",
|
"errorCheckingUpdatesNotifDescription": "A notification that shows when background update checking fails",
|
||||||
"appsRemoved": "Apps Removed",
|
"appsRemoved": "Apps Removed",
|
||||||
@@ -282,12 +287,12 @@
|
|||||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||||
"selectX": "Select {}",
|
"selectX": "Select {}",
|
||||||
"parallelDownloads": "Allow parallel downloads",
|
"parallelDownloads": "Allow parallel downloads",
|
||||||
"installMethod": "Installation method",
|
"useShizuku": "Use Shizuku or Sui to install",
|
||||||
"normal": "Normal",
|
"shizukuBinderNotFound": "Shizuku service not found, probably it's not launched",
|
||||||
"root": "Root",
|
"shizukuOld": "Old Shizuku version (<11), update it",
|
||||||
"shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
|
"shizukuOldAndroidWithADB": "Shizuku running on Android < 8.1 with ADB, update Android or use Sui instead",
|
||||||
|
"shizukuPretendToBeGooglePlay": "Set Google Play as the installation source (if Shizuku is used)",
|
||||||
"useSystemFont": "Use the system font",
|
"useSystemFont": "Use the system font",
|
||||||
"systemFontError": "Error loading the system font: {}",
|
|
||||||
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
|
||||||
"requestHeader": "Request header",
|
"requestHeader": "Request header",
|
||||||
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
|
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
|
||||||
@@ -352,6 +357,10 @@
|
|||||||
"one": "{} and 1 more app was updated.",
|
"one": "{} and 1 more app was updated.",
|
||||||
"other": "{} and {} more apps were updated."
|
"other": "{} and {} more apps were updated."
|
||||||
},
|
},
|
||||||
|
"xAndNMoreUpdatesFailed": {
|
||||||
|
"one": "Failed to update {} and 1 more app.",
|
||||||
|
"other": "Failed to update {} and {} more apps."
|
||||||
|
},
|
||||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||||
"one": "{} and 1 more app may have been updated.",
|
"one": "{} and 1 more app may have been updated.",
|
||||||
"other": "{} and {} more apps may have been updated."
|
"other": "{} and {} more apps may have been updated."
|
||||||
|
@@ -22,6 +22,9 @@
|
|||||||
"requiredInBrackets": "(обязательно)",
|
"requiredInBrackets": "(обязательно)",
|
||||||
"dropdownNoOptsError": "Ошибка: в выпадающем списке должна быть выбрана хотя бы одна настройка",
|
"dropdownNoOptsError": "Ошибка: в выпадающем списке должна быть выбрана хотя бы одна настройка",
|
||||||
"colour": "Цвет",
|
"colour": "Цвет",
|
||||||
|
"standard": "Стандартный",
|
||||||
|
"custom": "Индивидуальный",
|
||||||
|
"useMaterialYou": "Использовать Material You",
|
||||||
"githubStarredRepos": "Избранные репозитории GitHub",
|
"githubStarredRepos": "Избранные репозитории GitHub",
|
||||||
"uname": "Имя пользователя",
|
"uname": "Имя пользователя",
|
||||||
"wrongArgNum": "Неправильное количество предоставленных аргументов",
|
"wrongArgNum": "Неправильное количество предоставленных аргументов",
|
||||||
@@ -143,8 +146,10 @@
|
|||||||
"noNewUpdates": "Нет новых обновлений",
|
"noNewUpdates": "Нет новых обновлений",
|
||||||
"xHasAnUpdate": "{} есть обновление",
|
"xHasAnUpdate": "{} есть обновление",
|
||||||
"appsUpdated": "Приложения обновлены",
|
"appsUpdated": "Приложения обновлены",
|
||||||
|
"appsNotUpdated": "Не удалось обновить приложения",
|
||||||
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
|
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
|
||||||
"xWasUpdatedToY": "{} была обновлена до версии {}",
|
"xWasUpdatedToY": "{} была обновлена до версии {}",
|
||||||
|
"xWasNotUpdatedToY": "Не удалось обновить {} до версии {}",
|
||||||
"errorCheckingUpdates": "Ошибка при проверке обновлений",
|
"errorCheckingUpdates": "Ошибка при проверке обновлений",
|
||||||
"errorCheckingUpdatesNotifDescription": "Уведомление о завершении проверки обновлений в фоновом режиме с ошибкой",
|
"errorCheckingUpdatesNotifDescription": "Уведомление о завершении проверки обновлений в фоновом режиме с ошибкой",
|
||||||
"appsRemoved": "Приложение удалено",
|
"appsRemoved": "Приложение удалено",
|
||||||
@@ -282,12 +287,12 @@
|
|||||||
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
||||||
"selectX": "Выбрать {}",
|
"selectX": "Выбрать {}",
|
||||||
"parallelDownloads": "Разрешить параллельные загрузки",
|
"parallelDownloads": "Разрешить параллельные загрузки",
|
||||||
"installMethod": "Метод установки",
|
"useShizuku": "Использовать Shizuku или Sui для установки",
|
||||||
"normal": "Нормальный",
|
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден, возможно он не запущен",
|
||||||
"root": "Суперпользователь",
|
"shizukuOld": "Устаревшая версия Shizuku (<11), обновите",
|
||||||
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
|
"shizukuOldAndroidWithADB": "Shizuku работает на Android < 8.1 с ADB, обновите Android или используйте Sui",
|
||||||
|
"shizukuPretendToBeGooglePlay": "Указать Google Play как источник установки (если используется Shizuku)",
|
||||||
"useSystemFont": "Использовать системный шрифт",
|
"useSystemFont": "Использовать системный шрифт",
|
||||||
"systemFontError": "Ошибка загрузки системного шрифта: {}",
|
|
||||||
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
|
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
|
||||||
"requestHeader": "Заголовок запроса",
|
"requestHeader": "Заголовок запроса",
|
||||||
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку ресурса в качестве даты выпуска",
|
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку ресурса в качестве даты выпуска",
|
||||||
@@ -352,6 +357,10 @@
|
|||||||
"one": "{} и ещё 1 приложение были обновлены",
|
"one": "{} и ещё 1 приложение были обновлены",
|
||||||
"other": "{} и ещё {} приложений были обновлены"
|
"other": "{} и ещё {} приложений были обновлены"
|
||||||
},
|
},
|
||||||
|
"xAndNMoreUpdatesFailed": {
|
||||||
|
"one": "Не удалось обновить {} и ещё 1 приложение",
|
||||||
|
"other": "Не удалось обновить {} и ещё {} приложений"
|
||||||
|
},
|
||||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||||
"one": "{} и ещё 1 приложение могли быть обновлены",
|
"one": "{} и ещё 1 приложение могли быть обновлены",
|
||||||
"other": "{} и ещё {} приложений могли быть обновлены"
|
"other": "{} и ещё {} приложений могли быть обновлены"
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:obtainium/pages/home.dart';
|
import 'package:obtainium/pages/home.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
|
import 'package:obtainium/providers/native_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@@ -118,8 +119,6 @@ void main() async {
|
|||||||
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultThemeColour = Colors.deepPurple;
|
|
||||||
|
|
||||||
class Obtainium extends StatefulWidget {
|
class Obtainium extends StatefulWidget {
|
||||||
const Obtainium({super.key});
|
const Obtainium({super.key});
|
||||||
|
|
||||||
@@ -213,15 +212,13 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
// Decide on a colour/brightness scheme based on OS and user settings
|
// Decide on a colour/brightness scheme based on OS and user settings
|
||||||
ColorScheme lightColorScheme;
|
ColorScheme lightColorScheme;
|
||||||
ColorScheme darkColorScheme;
|
ColorScheme darkColorScheme;
|
||||||
if (lightDynamic != null &&
|
if (lightDynamic != null && darkDynamic != null && settingsProvider.useMaterialYou) {
|
||||||
darkDynamic != null &&
|
|
||||||
settingsProvider.colour == ColourSettings.materialYou) {
|
|
||||||
lightColorScheme = lightDynamic.harmonized();
|
lightColorScheme = lightDynamic.harmonized();
|
||||||
darkColorScheme = darkDynamic.harmonized();
|
darkColorScheme = darkDynamic.harmonized();
|
||||||
} else {
|
} else {
|
||||||
lightColorScheme = ColorScheme.fromSeed(seedColor: defaultThemeColour);
|
lightColorScheme = ColorScheme.fromSeed(seedColor: settingsProvider.themeColor);
|
||||||
darkColorScheme = ColorScheme.fromSeed(
|
darkColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: defaultThemeColour, brightness: Brightness.dark);
|
seedColor: settingsProvider.themeColor, brightness: Brightness.dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the background and surface colors to pure black in the amoled theme
|
// set the background and surface colors to pure black in the amoled theme
|
||||||
@@ -231,6 +228,8 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
.harmonized();
|
.harmonized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Obtainium',
|
title: 'Obtainium',
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:equations/equations.dart';
|
||||||
|
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
@@ -12,6 +14,7 @@ import 'package:obtainium/providers/settings_provider.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:shizuku_apk_installer/shizuku_apk_installer.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatefulWidget {
|
class SettingsPage extends StatefulWidget {
|
||||||
@@ -22,78 +25,196 @@ class SettingsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
|
List<int> updateIntervalNodes = [
|
||||||
|
15, 30, 60, 120, 180, 360, 720, 1440, 4320, 10080, 20160, 43200];
|
||||||
|
int updateInterval = 0;
|
||||||
|
late SplineInterpolation updateIntervalInterpolator; // 🤓
|
||||||
|
String updateIntervalLabel = tr('neverManualOnly');
|
||||||
|
bool showIntervalLabel = true;
|
||||||
|
final Map<ColorSwatch<Object>, String> colorsNameMap =
|
||||||
|
<ColorSwatch<Object>, String> {
|
||||||
|
ColorTools.createPrimarySwatch(obtainiumThemeColor): 'Obtainium'
|
||||||
|
};
|
||||||
|
|
||||||
|
void initUpdateIntervalInterpolator() {
|
||||||
|
List<InterpolationNode> nodes = [];
|
||||||
|
for (final (index, element) in updateIntervalNodes.indexed) {
|
||||||
|
nodes.add(InterpolationNode(x: index.toDouble()+1, y: element.toDouble()));
|
||||||
|
}
|
||||||
|
updateIntervalInterpolator = SplineInterpolation(nodes: nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void processIntervalSliderValue(double val) {
|
||||||
|
if (val < 0.5) {
|
||||||
|
updateInterval = 0;
|
||||||
|
updateIntervalLabel = tr('neverManualOnly');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int valInterpolated = 0;
|
||||||
|
if (val < 1) {
|
||||||
|
valInterpolated = 15;
|
||||||
|
} else {
|
||||||
|
valInterpolated = updateIntervalInterpolator.compute(val).round();
|
||||||
|
}
|
||||||
|
if (valInterpolated < 60) {
|
||||||
|
updateInterval = valInterpolated;
|
||||||
|
updateIntervalLabel = plural('minute', valInterpolated);
|
||||||
|
} else if (valInterpolated < 8 * 60) {
|
||||||
|
int valRounded = (valInterpolated / 15).floor() * 15;
|
||||||
|
updateInterval = valRounded;
|
||||||
|
updateIntervalLabel = plural('hour', valRounded ~/ 60);
|
||||||
|
int mins = valRounded % 60;
|
||||||
|
if (mins != 0) updateIntervalLabel += " ${plural('minute', mins)}";
|
||||||
|
} else if (valInterpolated < 24 * 60) {
|
||||||
|
int valRounded = (valInterpolated / 30).floor() * 30;
|
||||||
|
updateInterval = valRounded;
|
||||||
|
updateIntervalLabel = plural('hour', valRounded / 60);
|
||||||
|
} else if (valInterpolated < 7 * 24 * 60){
|
||||||
|
int valRounded = (valInterpolated / (12 * 60)).floor() * 12 * 60;
|
||||||
|
updateInterval = valRounded;
|
||||||
|
updateIntervalLabel = plural('day', valRounded / (24 * 60));
|
||||||
|
} else {
|
||||||
|
int valRounded = (valInterpolated / (24 * 60)).floor() * 24 * 60;
|
||||||
|
updateInterval = valRounded;
|
||||||
|
updateIntervalLabel = plural('day', valRounded ~/ (24 * 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) settingsProvider.initializeSettings();
|
||||||
settingsProvider.initializeSettings();
|
initUpdateIntervalInterpolator();
|
||||||
|
processIntervalSliderValue(settingsProvider.updateIntervalSliderVal);
|
||||||
|
|
||||||
|
var themeDropdown = FutureBuilder(
|
||||||
|
builder: (ctx, val) {
|
||||||
|
return DropdownButtonFormField(
|
||||||
|
decoration: InputDecoration(labelText: tr('theme')),
|
||||||
|
value: settingsProvider.theme,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: ThemeSettings.light,
|
||||||
|
child: Text(tr('light')),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: ThemeSettings.dark,
|
||||||
|
child: Text(tr('dark')),
|
||||||
|
),
|
||||||
|
if ((val.data?.version.sdkInt ?? 0) >= 29) DropdownMenuItem(
|
||||||
|
value: ThemeSettings.system,
|
||||||
|
child: Text(tr('followSystem')),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
settingsProvider.theme = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
future: DeviceInfoPlugin().androidInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool> colorPickerDialog() async {
|
||||||
|
return ColorPicker(
|
||||||
|
color: settingsProvider.themeColor,
|
||||||
|
onColorChanged: (Color color) =>
|
||||||
|
setState(() =>
|
||||||
|
settingsProvider.themeColor = color
|
||||||
|
),
|
||||||
|
actionButtons: const ColorPickerActionButtons(
|
||||||
|
okButton: true,
|
||||||
|
closeButton: true,
|
||||||
|
dialogActionButtons: false,
|
||||||
|
),
|
||||||
|
pickersEnabled: const <ColorPickerType, bool>{
|
||||||
|
ColorPickerType.both: false,
|
||||||
|
ColorPickerType.primary: false,
|
||||||
|
ColorPickerType.accent: false,
|
||||||
|
ColorPickerType.bw: false,
|
||||||
|
ColorPickerType.custom: true,
|
||||||
|
ColorPickerType.wheel: true,
|
||||||
|
},
|
||||||
|
pickerTypeLabels: <ColorPickerType, String>{
|
||||||
|
ColorPickerType.custom: tr('standard'),
|
||||||
|
ColorPickerType.wheel: tr('custom')
|
||||||
|
},
|
||||||
|
title: Text(tr('selectX', args: [tr('colour')]),
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
wheelDiameter: 192,
|
||||||
|
wheelSquareBorderRadius: 32,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 24,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
enableShadesSelection: false,
|
||||||
|
customColorSwatchesAndNames: colorsNameMap,
|
||||||
|
showMaterialName: true,
|
||||||
|
showColorName: true,
|
||||||
|
materialNameTextStyle: Theme.of(context).textTheme.bodySmall,
|
||||||
|
colorNameTextStyle: Theme.of(context).textTheme.bodySmall,
|
||||||
|
copyPasteBehavior: const ColorPickerCopyPasteBehavior(longPressMenu: true),
|
||||||
|
).showPickerDialog(
|
||||||
|
context,
|
||||||
|
transitionBuilder: (BuildContext context,
|
||||||
|
Animation<double> a1, Animation<double> a2, Widget widget) {
|
||||||
|
final double curvedValue = Curves.easeInCubic.transform(a1.value);
|
||||||
|
return Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.diagonal3Values(curvedValue, curvedValue, 1),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: curvedValue,
|
||||||
|
child: widget
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
transitionDuration: const Duration(milliseconds: 250),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var installMethodDropdown = DropdownButtonFormField(
|
var colorPicker = ListTile(
|
||||||
decoration: InputDecoration(labelText: tr('installMethod')),
|
dense: true,
|
||||||
value: settingsProvider.installMethod,
|
contentPadding: EdgeInsets.zero,
|
||||||
items: [
|
title: Text(tr('selectX', args: [tr('colour')])),
|
||||||
DropdownMenuItem(
|
subtitle: Text("${ColorTools.nameThatColor(settingsProvider.themeColor)} "
|
||||||
value: InstallMethodSettings.normal,
|
"(${ColorTools.materialNameAndCode(settingsProvider.themeColor,
|
||||||
child: Text(tr('normal')),
|
colorSwatchNameMap: colorsNameMap)})"),
|
||||||
),
|
trailing: ColorIndicator(
|
||||||
const DropdownMenuItem(
|
width: 40,
|
||||||
value: InstallMethodSettings.shizuku,
|
height: 40,
|
||||||
child: Text('Shizuku'),
|
borderRadius: 20,
|
||||||
),
|
color: settingsProvider.themeColor,
|
||||||
DropdownMenuItem(
|
onSelectFocus: false,
|
||||||
value: InstallMethodSettings.root,
|
onSelect: () async {
|
||||||
child: Text(tr('root')),
|
final Color colorBeforeDialog = settingsProvider.themeColor;
|
||||||
)
|
if (!(await colorPickerDialog())) {
|
||||||
],
|
setState(() {
|
||||||
onChanged: (value) {
|
settingsProvider.themeColor = colorBeforeDialog;
|
||||||
if (value != null) {
|
});
|
||||||
settingsProvider.installMethod = value;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var themeDropdown = DropdownButtonFormField(
|
var useMaterialThemeSwitch = FutureBuilder(
|
||||||
decoration: InputDecoration(labelText: tr('theme')),
|
builder: (ctx, val) {
|
||||||
value: settingsProvider.theme,
|
return ((val.data?.version.sdkInt ?? 0) >= 31) ?
|
||||||
items: [
|
Row(
|
||||||
DropdownMenuItem(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
value: ThemeSettings.dark,
|
children: [
|
||||||
child: Text(tr('dark')),
|
Flexible(child: Text(tr('useMaterialYou'))),
|
||||||
),
|
Switch(
|
||||||
DropdownMenuItem(
|
value: settingsProvider.useMaterialYou,
|
||||||
value: ThemeSettings.light,
|
onChanged: (value) {
|
||||||
child: Text(tr('light')),
|
settingsProvider.useMaterialYou = value;
|
||||||
),
|
})
|
||||||
DropdownMenuItem(
|
],
|
||||||
value: ThemeSettings.system,
|
) : const SizedBox.shrink();
|
||||||
child: Text(tr('followSystem')),
|
},
|
||||||
)
|
future: DeviceInfoPlugin().androidInfo
|
||||||
],
|
);
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
settingsProvider.theme = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var colourDropdown = DropdownButtonFormField(
|
|
||||||
decoration: InputDecoration(labelText: tr('colour')),
|
|
||||||
value: settingsProvider.colour,
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: ColourSettings.basic,
|
|
||||||
child: Text('Obtainium'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: ColourSettings.materialYou,
|
|
||||||
child: Text('Material You'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
settingsProvider.colour = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var sortDropdown = DropdownButtonFormField(
|
var sortDropdown = DropdownButtonFormField(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
@@ -165,30 +286,29 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var intervalDropdown = DropdownButtonFormField(
|
var intervalSlider = Slider(
|
||||||
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
value: settingsProvider.updateIntervalSliderVal,
|
||||||
value: settingsProvider.updateInterval,
|
max: updateIntervalNodes.length.toDouble(),
|
||||||
items: updateIntervals.map((e) {
|
divisions: updateIntervalNodes.length * 20,
|
||||||
int displayNum = (e < 60
|
label: updateIntervalLabel,
|
||||||
? e
|
onChanged: (double value) {
|
||||||
: e < 1440
|
setState(() {
|
||||||
? e / 60
|
settingsProvider.updateIntervalSliderVal = value;
|
||||||
: e / 1440)
|
processIntervalSliderValue(value);
|
||||||
.round();
|
|
||||||
String display = e == 0
|
|
||||||
? tr('neverManualOnly')
|
|
||||||
: (e < 60
|
|
||||||
? plural('minute', displayNum)
|
|
||||||
: e < 1440
|
|
||||||
? plural('hour', displayNum)
|
|
||||||
: plural('day', displayNum));
|
|
||||||
return DropdownMenuItem(value: e, child: Text(display));
|
|
||||||
}).toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
settingsProvider.updateInterval = value;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
onChangeStart: (double value) {
|
||||||
|
setState(() {
|
||||||
|
showIntervalLabel = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (double value) {
|
||||||
|
setState(() {
|
||||||
|
showIntervalLabel = true;
|
||||||
|
settingsProvider.updateInterval = updateInterval;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
||||||
if (e.sourceConfigSettingFormItems.isNotEmpty) {
|
if (e.sourceConfigSettingFormItems.isNotEmpty) {
|
||||||
@@ -239,15 +359,19 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
intervalDropdown,
|
//intervalDropdown,
|
||||||
|
height16,
|
||||||
|
if (showIntervalLabel) SizedBox(
|
||||||
|
child: Text("${tr('bgUpdateCheckInterval')}: $updateIntervalLabel")
|
||||||
|
) else const SizedBox(height: 16),
|
||||||
|
intervalSlider,
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
builder: (ctx, val) {
|
builder: (ctx, val) {
|
||||||
return (val.data?.version.sdkInt ?? 0) >= 30
|
return ((val.data?.version.sdkInt ?? 0) >= 30) || settingsProvider.useShizuku
|
||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
height16,
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment
|
MainAxisAlignment
|
||||||
@@ -385,38 +509,65 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(tr(
|
Text(tr(
|
||||||
'beforeNewInstallsShareToAppVerifier')),
|
'beforeNewInstallsShareToAppVerifier')),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://github.com/soupslurpr/AppVerifier',
|
'https://github.com/soupslurpr/AppVerifier',
|
||||||
mode: LaunchMode
|
mode: LaunchMode
|
||||||
.externalApplication);
|
.externalApplication);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('about'),
|
tr('about'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration:
|
decoration:
|
||||||
TextDecoration.underline,
|
TextDecoration.underline,
|
||||||
fontSize: 12),
|
fontSize: 12),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider
|
value: settingsProvider
|
||||||
.beforeNewInstallsShareToAppVerifier,
|
.beforeNewInstallsShareToAppVerifier,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
settingsProvider
|
settingsProvider
|
||||||
.beforeNewInstallsShareToAppVerifier =
|
.beforeNewInstallsShareToAppVerifier =
|
||||||
value;
|
value;
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
installMethodDropdown,
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('useShizuku'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.useShizuku,
|
||||||
|
onChanged: (useShizuku) {
|
||||||
|
if (useShizuku) {
|
||||||
|
ShizukuApkInstaller.checkPermission().then((resCode) {
|
||||||
|
settingsProvider.useShizuku = resCode!.startsWith('granted');
|
||||||
|
switch(resCode){
|
||||||
|
case 'binder_not_found':
|
||||||
|
showError(ObtainiumError(tr('shizukuBinderNotFound')), context);
|
||||||
|
case 'old_shizuku':
|
||||||
|
showError(ObtainiumError(tr('shizukuOld')), context);
|
||||||
|
case 'old_android_with_adb':
|
||||||
|
showError(ObtainiumError(tr('shizukuOldAndroidWithADB')), context);
|
||||||
|
case 'denied':
|
||||||
|
showError(ObtainiumError(tr('cancelled')), context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
settingsProvider.useShizuku = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
height32,
|
height32,
|
||||||
Text(
|
Text(
|
||||||
tr('sourceSpecific'),
|
tr('sourceSpecific'),
|
||||||
@@ -445,8 +596,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
colourDropdown,
|
|
||||||
height16,
|
height16,
|
||||||
|
useMaterialThemeSwitch,
|
||||||
|
if (!settingsProvider.useMaterialYou) colorPicker,
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -460,34 +612,35 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
localeDropdown,
|
localeDropdown,
|
||||||
height16,
|
FutureBuilder(
|
||||||
Row(
|
builder: (ctx, val) {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
return (val.data?.version.sdkInt ?? 0) >= 34
|
||||||
children: [
|
? Column(
|
||||||
Flexible(child: Text(tr('useSystemFont'))),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Switch(
|
children: [
|
||||||
value: settingsProvider.useSystemFont,
|
height16,
|
||||||
onChanged: (useSystemFont) {
|
Row(
|
||||||
if (useSystemFont) {
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
NativeFeatures.loadSystemFont()
|
children: [
|
||||||
.then((fontLoadRes) {
|
Flexible(child: Text(tr('useSystemFont'))),
|
||||||
if (fontLoadRes == 'ok') {
|
Switch(
|
||||||
settingsProvider.useSystemFont =
|
value: settingsProvider.useSystemFont,
|
||||||
true;
|
onChanged: (useSystemFont) {
|
||||||
} else {
|
if (useSystemFont) {
|
||||||
showError(
|
NativeFeatures.loadSystemFont().then((val) {
|
||||||
ObtainiumError(tr(
|
settingsProvider.useSystemFont = true;
|
||||||
'systemFontError',
|
});
|
||||||
args: [fontLoadRes])),
|
} else {
|
||||||
context);
|
settingsProvider.useSystemFont = false;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} else {
|
]
|
||||||
settingsProvider.useSystemFont = false;
|
)
|
||||||
}
|
]
|
||||||
})
|
)
|
||||||
],
|
: const SizedBox.shrink();
|
||||||
),
|
},
|
||||||
|
future: DeviceInfoPlugin().androidInfo),
|
||||||
height16,
|
height16,
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
@@ -34,7 +34,7 @@ import 'package:android_intent_plus/android_intent.dart';
|
|||||||
import 'package:flutter_archive/flutter_archive.dart';
|
import 'package:flutter_archive/flutter_archive.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||||
import 'native_provider.dart';
|
import 'package:shizuku_apk_installer/shizuku_apk_installer.dart';
|
||||||
|
|
||||||
final pm = AndroidPackageManager();
|
final pm = AndroidPackageManager();
|
||||||
|
|
||||||
@@ -507,9 +507,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
Future<bool> canInstallSilently(App app) async {
|
Future<bool> canInstallSilently(App app) async {
|
||||||
if (app.id == obtainiumId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!settingsProvider.enableBackgroundUpdates) {
|
if (!settingsProvider.enableBackgroundUpdates) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -517,8 +514,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (app.apkUrls.length > 1) {
|
if (app.apkUrls.length > 1) {
|
||||||
// Manual API selection means silent install is not possible
|
return false; // Manual API selection means silent install is not possible
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var osInfo = await DeviceInfoPlugin().androidInfo;
|
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
@@ -529,20 +525,29 @@ class AppsProvider with ChangeNotifier {
|
|||||||
?.installingPackageName
|
?.installingPackageName
|
||||||
: (await pm.getInstallerPackageName(packageName: app.id));
|
: (await pm.getInstallerPackageName(packageName: app.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Probably not installed - ignore
|
return false; // App probably not installed
|
||||||
}
|
}
|
||||||
if (installerPackageName != obtainiumId) {
|
|
||||||
// If we did not install the app (or it isn't installed), silent install is not possible
|
int? targetSDK = (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
|
||||||
|
// The APK should target a new enough API
|
||||||
|
// https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
|
||||||
|
if (!(targetSDK != null && targetSDK >= (osInfo.version.sdkInt - 3))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int? targetSDK =
|
|
||||||
(await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
|
|
||||||
|
|
||||||
// The OS must also be new enough and the APK should target a new enough API
|
if (settingsProvider.useShizuku) {
|
||||||
return osInfo.version.sdkInt >= 31 &&
|
return true;
|
||||||
targetSDK != null &&
|
}
|
||||||
targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
|
|
||||||
(osInfo.version.sdkInt - 3);
|
if (app.id == obtainiumId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (installerPackageName != obtainiumId) {
|
||||||
|
// If we did not install the app, silent install is not possible
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The OS must also be new enough
|
||||||
|
return osInfo.version.sdkInt >= 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> waitForUserToReturnToForeground(BuildContext context) async {
|
Future<void> waitForUserToReturnToForeground(BuildContext context) async {
|
||||||
@@ -566,7 +571,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<bool> installXApkDir(
|
Future<bool> installXApkDir(
|
||||||
DownloadedXApkDir dir, BuildContext? firstTimeWithContext,
|
DownloadedXApkDir dir, BuildContext? firstTimeWithContext,
|
||||||
{bool needsBGWorkaround = false}) async {
|
{bool needsBGWorkaround = false, bool shizukuPretendToBeGooglePlay = false}) async {
|
||||||
// We don't know which APKs in an XAPK are supported by the user's device
|
// We don't know which APKs in an XAPK are supported by the user's device
|
||||||
// So we try installing all of them and assume success if at least one installed
|
// So we try installing all of them and assume success if at least one installed
|
||||||
// If 0 APKs installed, throw the first install error encountered
|
// If 0 APKs installed, throw the first install error encountered
|
||||||
@@ -581,7 +586,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
somethingInstalled = somethingInstalled ||
|
somethingInstalled = somethingInstalled ||
|
||||||
await installApk(
|
await installApk(
|
||||||
DownloadedApk(dir.appId, file), firstTimeWithContext,
|
DownloadedApk(dir.appId, file), firstTimeWithContext,
|
||||||
needsBGWorkaround: needsBGWorkaround);
|
needsBGWorkaround: needsBGWorkaround,
|
||||||
|
shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logs.add(
|
logs.add(
|
||||||
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
|
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
|
||||||
@@ -604,7 +610,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<bool> installApk(
|
Future<bool> installApk(
|
||||||
DownloadedApk file, BuildContext? firstTimeWithContext,
|
DownloadedApk file, BuildContext? firstTimeWithContext,
|
||||||
{bool needsBGWorkaround = false}) async {
|
{bool needsBGWorkaround = false, bool shizukuPretendToBeGooglePlay = false}) async {
|
||||||
if (firstTimeWithContext != null &&
|
if (firstTimeWithContext != null &&
|
||||||
settingsProvider.beforeNewInstallsShareToAppVerifier &&
|
settingsProvider.beforeNewInstallsShareToAppVerifier &&
|
||||||
(await getInstalledInfo('dev.soupslurpr.appverifier')) != null) {
|
(await getInstalledInfo('dev.soupslurpr.appverifier')) != null) {
|
||||||
@@ -632,8 +638,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
if (needsBGWorkaround &&
|
if (needsBGWorkaround) {
|
||||||
settingsProvider.installMethod == InstallMethodSettings.normal) {
|
|
||||||
// The below 'await' will never return if we are in a background process
|
// The below 'await' will never return if we are in a background process
|
||||||
// To work around this, we should assume the install will be successful
|
// To work around this, we should assume the install will be successful
|
||||||
// So we update the app's installed version first as we will never get to the later code
|
// So we update the app's installed version first as we will never get to the later code
|
||||||
@@ -645,20 +650,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
attemptToCorrectInstallStatus: false);
|
attemptToCorrectInstallStatus: false);
|
||||||
}
|
}
|
||||||
int? code;
|
int? code;
|
||||||
switch (settingsProvider.installMethod) {
|
if (!settingsProvider.useShizuku) {
|
||||||
case InstallMethodSettings.normal:
|
code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||||
code = await AndroidPackageInstaller.installApk(
|
} else {
|
||||||
apkFilePath: file.file.path);
|
code = await ShizukuApkInstaller.installAPK(file.file.uri.toString(),
|
||||||
case InstallMethodSettings.shizuku:
|
shizukuPretendToBeGooglePlay ? "com.android.vending" : "");
|
||||||
code = (await NativeFeatures.installWithShizuku(
|
|
||||||
apkFileUri: file.file.uri.toString()))
|
|
||||||
? 0
|
|
||||||
: 1;
|
|
||||||
case InstallMethodSettings.root:
|
|
||||||
code =
|
|
||||||
(await NativeFeatures.installWithRoot(apkFilePath: file.file.path))
|
|
||||||
? 0
|
|
||||||
: 1;
|
|
||||||
}
|
}
|
||||||
bool installed = false;
|
bool installed = false;
|
||||||
if (code != null && code != 0 && code != 3) {
|
if (code != null && code != 0 && code != 3) {
|
||||||
@@ -716,8 +712,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
if (urlsToSelectFrom.length > 1 && context != null) {
|
if (urlsToSelectFrom.length > 1 && context != null) {
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
appFileUrl = await showDialog(
|
appFileUrl = await showDialog(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AppFilePicker(
|
return AppFilePicker(
|
||||||
@@ -737,10 +733,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (appFileUrl != null &&
|
if (appFileUrl != null &&
|
||||||
getHost(appFileUrl.value) != getHost(app.url) &&
|
getHost(appFileUrl.value) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
if (!(settingsProvider.hideAPKOriginWarning) &&
|
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return APKOriginWarningDialog(
|
return APKOriginWarningDialog(
|
||||||
@@ -828,23 +823,21 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
id = downloadedFile?.appId ?? downloadedDir!.appId;
|
id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||||
bool willBeSilent = await canInstallSilently(apps[id]!.app);
|
bool willBeSilent = await canInstallSilently(apps[id]!.app);
|
||||||
switch (settingsProvider.installMethod) {
|
if (!settingsProvider.useShizuku) {
|
||||||
case InstallMethodSettings.normal:
|
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
||||||
if (!(await settingsProvider.getInstallPermission(
|
throw ObtainiumError(tr('cancelled'));
|
||||||
enforce: false))) {
|
}
|
||||||
throw ObtainiumError(tr('cancelled'));
|
} else {
|
||||||
}
|
switch((await ShizukuApkInstaller.checkPermission())!){
|
||||||
case InstallMethodSettings.shizuku:
|
case 'binder_not_found':
|
||||||
int code = await NativeFeatures.checkPermissionShizuku();
|
|
||||||
if (code == -1) {
|
|
||||||
throw ObtainiumError(tr('shizukuBinderNotFound'));
|
throw ObtainiumError(tr('shizukuBinderNotFound'));
|
||||||
} else if (code == 0) {
|
case 'old_shizuku':
|
||||||
|
throw ObtainiumError(tr('shizukuOld'));
|
||||||
|
case 'old_android_with_adb':
|
||||||
|
throw ObtainiumError(tr('shizukuOldAndroidWithADB'));
|
||||||
|
case 'denied':
|
||||||
throw ObtainiumError(tr('cancelled'));
|
throw ObtainiumError(tr('cancelled'));
|
||||||
}
|
}
|
||||||
case InstallMethodSettings.root:
|
|
||||||
if (!(await NativeFeatures.checkPermissionRoot())) {
|
|
||||||
throw ObtainiumError(tr('cancelled'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!willBeSilent && context != null) {
|
if (!willBeSilent && context != null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@@ -857,27 +850,32 @@ class AppsProvider with ChangeNotifier {
|
|||||||
bool sayInstalled = true;
|
bool sayInstalled = true;
|
||||||
var contextIfNewInstall =
|
var contextIfNewInstall =
|
||||||
apps[id]?.installedInfo == null ? context : null;
|
apps[id]?.installedInfo == null ? context : null;
|
||||||
|
bool needBGWorkaround = willBeSilent && context == null && !settingsProvider.useShizuku;
|
||||||
if (downloadedFile != null) {
|
if (downloadedFile != null) {
|
||||||
if (willBeSilent && context == null) {
|
if (needBGWorkaround) {
|
||||||
installApk(downloadedFile, contextIfNewInstall,
|
// ignore: use_build_context_synchronously
|
||||||
needsBGWorkaround: true);
|
installApk(downloadedFile, contextIfNewInstall, needsBGWorkaround: true);
|
||||||
} else {
|
} else {
|
||||||
sayInstalled =
|
// ignore: use_build_context_synchronously
|
||||||
await installApk(downloadedFile, contextIfNewInstall);
|
sayInstalled = await installApk(downloadedFile, contextIfNewInstall, shizukuPretendToBeGooglePlay: apps[id]!.app.additionalSettings['shizukuPretendToBeGooglePlay'] == true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (willBeSilent && context == null) {
|
if (needBGWorkaround) {
|
||||||
installXApkDir(downloadedDir!, contextIfNewInstall,
|
// ignore: use_build_context_synchronously
|
||||||
needsBGWorkaround: true);
|
installXApkDir(downloadedDir!, contextIfNewInstall, needsBGWorkaround: true);
|
||||||
} else {
|
} else {
|
||||||
sayInstalled =
|
// ignore: use_build_context_synchronously
|
||||||
await installXApkDir(downloadedDir!, contextIfNewInstall);
|
sayInstalled = await installXApkDir(downloadedDir!, contextIfNewInstall, shizukuPretendToBeGooglePlay: apps[id]!.app.additionalSettings['shizukuPretendToBeGooglePlay'] == true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (willBeSilent && context == null) {
|
if (willBeSilent && context == null) {
|
||||||
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
if (!settingsProvider.useShizuku) {
|
||||||
[apps[id]!.app],
|
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
||||||
id: id.hashCode));
|
[apps[id]!.app], id: id.hashCode));
|
||||||
|
} else {
|
||||||
|
notificationsProvider?.notify(SilentUpdateNotification(
|
||||||
|
[apps[id]!.app], sayInstalled, id: id.hashCode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (sayInstalled) {
|
if (sayInstalled) {
|
||||||
installedIds.add(id);
|
installedIds.add(id);
|
||||||
@@ -1710,7 +1708,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
|
|||||||
int maxRetryWaitSeconds = 5;
|
int maxRetryWaitSeconds = 5;
|
||||||
|
|
||||||
var netResult = await (Connectivity().checkConnectivity());
|
var netResult = await (Connectivity().checkConnectivity());
|
||||||
if (netResult == ConnectivityResult.none) {
|
if (netResult.contains(ConnectivityResult.none)) {
|
||||||
logs.add('BG update task: No network.');
|
logs.add('BG update task: No network.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1747,8 +1745,8 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
|
|||||||
|
|
||||||
var networkRestricted = false;
|
var networkRestricted = false;
|
||||||
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
||||||
networkRestricted = (netResult != ConnectivityResult.wifi) &&
|
networkRestricted = !netResult.contains(ConnectivityResult.wifi) &&
|
||||||
(netResult != ConnectivityResult.ethernet);
|
!netResult.contains(ConnectivityResult.ethernet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toCheck.isNotEmpty) {
|
if (toCheck.isNotEmpty) {
|
||||||
@@ -1792,8 +1790,8 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
|
|||||||
var networkRestricted = false;
|
var networkRestricted = false;
|
||||||
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
||||||
var netResult = await (Connectivity().checkConnectivity());
|
var netResult = await (Connectivity().checkConnectivity());
|
||||||
networkRestricted = (netResult != ConnectivityResult.wifi) &&
|
networkRestricted = !netResult.contains(ConnectivityResult.wifi) &&
|
||||||
(netResult != ConnectivityResult.ethernet);
|
!netResult.contains(ConnectivityResult.ethernet);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -1,75 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:android_system_font/android_system_font.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class NativeFeatures {
|
class NativeFeatures {
|
||||||
static const MethodChannel _channel = MethodChannel('native');
|
|
||||||
static bool _systemFontLoaded = false;
|
static bool _systemFontLoaded = false;
|
||||||
static bool _callbacksApplied = false;
|
|
||||||
static int _resPermShizuku = -2; // not set
|
|
||||||
|
|
||||||
static Future<ByteData> _readFileBytes(String path) async {
|
static Future<ByteData> _readFileBytes(String path) async {
|
||||||
var file = File(path);
|
var bytes = await File(path).readAsBytes();
|
||||||
var bytes = await file.readAsBytes();
|
|
||||||
return ByteData.view(bytes.buffer);
|
return ByteData.view(bytes.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future _handleCalls(MethodCall call) async {
|
static Future loadSystemFont() async {
|
||||||
if (call.method == 'resPermShizuku') {
|
if (_systemFontLoaded) return;
|
||||||
_resPermShizuku = call.arguments['res'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String> loadSystemFont() async {
|
|
||||||
if (_systemFontLoaded) { return "ok"; }
|
|
||||||
var getFontRes = await _channel.invokeMethod('getSystemFont');
|
|
||||||
if (getFontRes[0] != '/') { return getFontRes; } // Error
|
|
||||||
var fontLoader = FontLoader('SystemFont');
|
var fontLoader = FontLoader('SystemFont');
|
||||||
fontLoader.addFont(_readFileBytes(getFontRes));
|
var fontFilePath = await AndroidSystemFont().getFilePath();
|
||||||
await fontLoader.load();
|
fontLoader.addFont(_readFileBytes(fontFilePath!));
|
||||||
|
fontLoader.load();
|
||||||
_systemFontLoaded = true;
|
_systemFontLoaded = true;
|
||||||
return "ok";
|
|
||||||
}
|
|
||||||
|
|
||||||
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});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -41,20 +41,26 @@ class UpdateNotification extends ObtainiumNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SilentUpdateNotification extends ObtainiumNotification {
|
class SilentUpdateNotification extends ObtainiumNotification {
|
||||||
SilentUpdateNotification(List<App> updates, {int? id})
|
SilentUpdateNotification(List<App> updates, bool succeeded, {int? id})
|
||||||
: super(
|
: super(
|
||||||
id ?? 3,
|
id ?? 3,
|
||||||
tr('appsUpdated'),
|
succeeded
|
||||||
|
? tr('appsUpdated')
|
||||||
|
: tr('appsNotUpdated'),
|
||||||
'',
|
'',
|
||||||
'APPS_UPDATED',
|
'APPS_UPDATED',
|
||||||
tr('appsUpdatedNotifChannel'),
|
tr('appsUpdatedNotifChannel'),
|
||||||
tr('appsUpdatedNotifDescription'),
|
tr('appsUpdatedNotifDescription'),
|
||||||
Importance.defaultImportance) {
|
Importance.defaultImportance) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr('xWasUpdatedToY',
|
? tr(succeeded
|
||||||
args: [updates[0].finalName, updates[0].latestVersion])
|
? 'xWasUpdatedToY'
|
||||||
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
|
: 'xWasNotUpdatedToY',
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
args: [updates[0].finalName, updates[0].latestVersion])
|
||||||
|
: plural(succeeded
|
||||||
|
? 'xAndNMoreUpdatesInstalled'
|
||||||
|
: "xAndNMoreUpdatesFailed",
|
||||||
|
updates.length - 1, args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,41 +17,14 @@ import 'package:shared_storage/shared_storage.dart' as saf;
|
|||||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}';
|
String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}';
|
||||||
String obtainiumId = 'dev.imranr.obtainium';
|
String obtainiumId = 'dev.imranr.obtainium';
|
||||||
String obtainiumUrl = 'https://github.com/ImranR98/Obtainium';
|
String obtainiumUrl = 'https://github.com/ImranR98/Obtainium';
|
||||||
|
Color obtainiumThemeColor = const Color(0xFF6438B5);
|
||||||
|
|
||||||
enum InstallMethodSettings { normal, shizuku, root }
|
enum ThemeSettings { light, dark, system }
|
||||||
|
|
||||||
enum ThemeSettings { system, light, dark }
|
|
||||||
|
|
||||||
enum ColourSettings { basic, materialYou }
|
|
||||||
|
|
||||||
enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
|
enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
|
||||||
|
|
||||||
enum SortOrderSettings { ascending, descending }
|
enum SortOrderSettings { ascending, descending }
|
||||||
|
|
||||||
const maxAPIRateLimitMinutes = 30;
|
|
||||||
const minUpdateIntervalMinutes = maxAPIRateLimitMinutes + 30;
|
|
||||||
const maxUpdateIntervalMinutes = 43200;
|
|
||||||
List<int> updateIntervals = [
|
|
||||||
15,
|
|
||||||
30,
|
|
||||||
60,
|
|
||||||
120,
|
|
||||||
180,
|
|
||||||
360,
|
|
||||||
720,
|
|
||||||
1440,
|
|
||||||
4320,
|
|
||||||
10080,
|
|
||||||
20160,
|
|
||||||
43200,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
.where((element) =>
|
|
||||||
(element >= minUpdateIntervalMinutes &&
|
|
||||||
element <= maxUpdateIntervalMinutes) ||
|
|
||||||
element == 0)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
class SettingsProvider with ChangeNotifier {
|
class SettingsProvider with ChangeNotifier {
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
String? defaultAppDir;
|
String? defaultAppDir;
|
||||||
@@ -75,19 +48,18 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallMethodSettings get installMethod {
|
bool get useShizuku{
|
||||||
return InstallMethodSettings.values[
|
return prefs?.getBool('useShizuku') ?? false;
|
||||||
prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set installMethod(InstallMethodSettings t) {
|
set useShizuku(bool useShizuku) {
|
||||||
prefs?.setInt('installMethod', t.index);
|
prefs?.setBool('useShizuku', useShizuku);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeSettings get theme {
|
ThemeSettings get theme {
|
||||||
return ThemeSettings
|
return ThemeSettings
|
||||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
.values[prefs?.getInt('theme') ?? ThemeSettings.light.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set theme(ThemeSettings t) {
|
set theme(ThemeSettings t) {
|
||||||
@@ -95,13 +67,23 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
ColourSettings get colour {
|
Color get themeColor {
|
||||||
return ColourSettings
|
int? colorCode = prefs?.getInt('themeColor');
|
||||||
.values[prefs?.getInt('colour') ?? ColourSettings.basic.index];
|
return (colorCode != null) ?
|
||||||
|
Color(colorCode) : obtainiumThemeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
set colour(ColourSettings t) {
|
set themeColor(Color themeColor) {
|
||||||
prefs?.setInt('colour', t.index);
|
prefs?.setInt('themeColor', themeColor.value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get useMaterialYou {
|
||||||
|
return prefs?.getBool('useMaterialYou') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set useMaterialYou(bool useMaterialYou) {
|
||||||
|
prefs?.setBool('useMaterialYou', useMaterialYou);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,21 +97,20 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int get updateInterval {
|
int get updateInterval {
|
||||||
var min = prefs?.getInt('updateInterval') ?? 360;
|
return prefs?.getInt('updateInterval') ?? 360;
|
||||||
if (!updateIntervals.contains(min)) {
|
|
||||||
var temp = updateIntervals[0];
|
|
||||||
for (var i in updateIntervals) {
|
|
||||||
if (min > i && i != 0) {
|
|
||||||
temp = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
min = temp;
|
|
||||||
}
|
|
||||||
return min;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set updateInterval(int min) {
|
set updateInterval(int min) {
|
||||||
prefs?.setInt('updateInterval', (min < 15 && min != 0) ? 15 : min);
|
prefs?.setInt('updateInterval', min);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get updateIntervalSliderVal {
|
||||||
|
return prefs?.getDouble('updateIntervalSliderVal') ?? 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set updateIntervalSliderVal(double val) {
|
||||||
|
prefs?.setDouble('updateIntervalSliderVal', val);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -521,6 +521,11 @@ abstract class AppSource {
|
|||||||
label: tr('autoApkFilterByArch'), defaultValue: true)
|
label: tr('autoApkFilterByArch'), defaultValue: true)
|
||||||
],
|
],
|
||||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('shizukuPretendToBeGooglePlay',
|
||||||
|
label: tr('shizukuPretendToBeGooglePlay'),
|
||||||
|
defaultValue: false)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('exemptFromBackgroundUpdates',
|
GeneratedFormSwitch('exemptFromBackgroundUpdates',
|
||||||
label: tr('exemptFromBackgroundUpdates'))
|
label: tr('exemptFromBackgroundUpdates'))
|
||||||
|
60
pubspec.lock
60
pubspec.lock
@@ -26,6 +26,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.1"
|
version: "0.7.1"
|
||||||
|
android_system_font:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: master
|
||||||
|
resolved-ref: "355f897e92a58a803f91d9270d389d9ec40ba550"
|
||||||
|
url: "https://github.com/re7gog/android_system_font"
|
||||||
|
source: git
|
||||||
|
version: "1.0.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -226,6 +235,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
version: "0.0.2"
|
||||||
|
equations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: equations
|
||||||
|
sha256: ae30e977d601e19aa1fc3409736c5eac01559d1d653a4c30141fbc4e86aa605c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -266,6 +283,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
flex_color_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flex_color_picker
|
||||||
|
sha256: "5c846437069fb7afdd7ade6bf37e628a71d2ab0787095ddcb1253bf9345d5f3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
flex_seed_scheme:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flex_seed_scheme
|
||||||
|
sha256: "4cee2f1d07259f77e8b36f4ec5f35499d19e74e17c7dce5b819554914082bc01"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -366,6 +399,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.5"
|
version: "8.2.5"
|
||||||
|
fraction:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fraction
|
||||||
|
sha256: "09e9504c9177bbd77df56e5d147abfbb3b43360e64bf61510059c14d6a82d524"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
gtk:
|
gtk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -634,10 +675,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
version: "5.4.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -750,6 +791,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.8.1"
|
||||||
|
shizuku_apk_installer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: master
|
||||||
|
resolved-ref: "25acc02612c2e0fcae40d312e047ac48106f8f6b"
|
||||||
|
url: "https://github.com/re7gog/shizuku_apk_installer"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1007,10 +1057,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.3.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1020,5 +1070,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
dart: ">=3.3.3 <4.0.0"
|
||||||
flutter: ">=3.19.0"
|
flutter: ">=3.19.0"
|
||||||
|
10
pubspec.yaml
10
pubspec.yaml
@@ -68,6 +68,16 @@ dependencies:
|
|||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
app_links: ^4.0.0
|
app_links: ^4.0.0
|
||||||
background_fetch: ^1.2.1
|
background_fetch: ^1.2.1
|
||||||
|
equations: ^5.0.2
|
||||||
|
flex_color_picker: ^3.4.1
|
||||||
|
android_system_font:
|
||||||
|
git:
|
||||||
|
url: https://github.com/re7gog/android_system_font
|
||||||
|
ref: master
|
||||||
|
shizuku_apk_installer:
|
||||||
|
git:
|
||||||
|
url: https://github.com/re7gog/shizuku_apk_installer
|
||||||
|
ref: master
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user