Merge pull request #1548 from re7gog/re7gog

Native and UI improvements
This commit is contained in:
Imran
2024-04-20 19:35:37 -04:00
committed by GitHub
22 changed files with 522 additions and 787 deletions

View File

@@ -92,20 +92,6 @@ repositories {
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]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->

View File

@@ -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"
}
}

View File

@@ -1,179 +1,5 @@
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 org.lsposed.hiddenapibypass.HiddenApiBypass
import rikka.shizuku.Shizuku
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
import rikka.shizuku.ShizukuBinderWrapper
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)
}
}
class MainActivity: FlutterActivity()

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}*/
}

View File

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

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -22,6 +22,9 @@
"requiredInBrackets": "(Required)",
"dropdownNoOptsError": "ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT",
"colour": "Colour",
"standard": "Standard",
"custom": "Custom",
"useMaterialYou": "Use Material You",
"githubStarredRepos": "GitHub Starred Repos",
"uname": "Username",
"wrongArgNum": "Wrong number of arguments provided",
@@ -143,8 +146,10 @@
"noNewUpdates": "No new updates.",
"xHasAnUpdate": "{} has an update.",
"appsUpdated": "Apps Updated",
"appsNotUpdated": "Failed to update applications",
"appsUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were applied in the background",
"xWasUpdatedToY": "{} was updated to {}.",
"xWasNotUpdatedToY": "Failed to update {} to {}.",
"errorCheckingUpdates": "Error Checking for Updates",
"errorCheckingUpdatesNotifDescription": "A notification that shows when background update checking fails",
"appsRemoved": "Apps Removed",
@@ -282,12 +287,12 @@
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"installMethod": "Installation method",
"normal": "Normal",
"root": "Root",
"shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
"useShizuku": "Use Shizuku or Sui to install",
"shizukuBinderNotFound": "Shizuku service not found, probably it's not launched",
"shizukuOld": "Old Shizuku version (<11), update it",
"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",
"systemFontError": "Error loading the system font: {}",
"useVersionCodeAsOSVersion": "Use app versionCode as OS-detected version",
"requestHeader": "Request header",
"useLatestAssetDateAsReleaseDate": "Use latest asset upload as release date",
@@ -352,6 +357,10 @@
"one": "{} and 1 more app was updated.",
"other": "{} and {} more apps were updated."
},
"xAndNMoreUpdatesFailed": {
"one": "Failed to update {} and 1 more app.",
"other": "Failed to update {} and {} more apps."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.",
"other": "{} and {} more apps may have been updated."

View File

@@ -22,6 +22,9 @@
"requiredInBrackets": "(обязательно)",
"dropdownNoOptsError": "Ошибка: в выпадающем списке должна быть выбрана хотя бы одна настройка",
"colour": "Цвет",
"standard": "Стандартный",
"custom": "Индивидуальный",
"useMaterialYou": "Использовать Material You",
"githubStarredRepos": "Избранные репозитории GitHub",
"uname": "Имя пользователя",
"wrongArgNum": "Неправильное количество предоставленных аргументов",
@@ -143,8 +146,10 @@
"noNewUpdates": "Нет новых обновлений",
"xHasAnUpdate": "{} есть обновление",
"appsUpdated": "Приложения обновлены",
"appsNotUpdated": "Не удалось обновить приложения",
"appsUpdatedNotifDescription": "Уведомляет об обновлении одного или нескольких приложений в фоновом режиме",
"xWasUpdatedToY": "{} была обновлена до версии {}",
"xWasNotUpdatedToY": "Не удалось обновить {} до версии {}",
"errorCheckingUpdates": "Ошибка при проверке обновлений",
"errorCheckingUpdatesNotifDescription": "Уведомление о завершении проверки обновлений в фоновом режиме с ошибкой",
"appsRemoved": "Приложение удалено",
@@ -282,12 +287,12 @@
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
"selectX": "Выбрать {}",
"parallelDownloads": "Разрешить параллельные загрузки",
"installMethod": "Метод установки",
"normal": "Нормальный",
"root": "Суперпользователь",
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
"useShizuku": "Использовать Shizuku или Sui для установки",
"shizukuBinderNotFound": "Совместимый сервис Shizuku не найден, возможно он не запущен",
"shizukuOld": "Устаревшая версия Shizuku (<11), обновите",
"shizukuOldAndroidWithADB": "Shizuku работает на Android < 8.1 с ADB, обновите Android или используйте Sui",
"shizukuPretendToBeGooglePlay": "Указать Google Play как источник установки (если используется Shizuku)",
"useSystemFont": "Использовать системный шрифт",
"systemFontError": "Ошибка загрузки системного шрифта: {}",
"useVersionCodeAsOSVersion": "Использовать код версии приложения как версию, обнаруженную ОС",
"requestHeader": "Заголовок запроса",
"useLatestAssetDateAsReleaseDate": "Использовать последнюю загрузку ресурса в качестве даты выпуска",
@@ -352,6 +357,10 @@
"one": "{} и ещё 1 приложение были обновлены",
"other": "{} и ещё {} приложений были обновлены"
},
"xAndNMoreUpdatesFailed": {
"one": "Не удалось обновить {} и ещё 1 приложение",
"other": "Не удалось обновить {} и ещё {} приложений"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} и ещё 1 приложение могли быть обновлены",
"other": "{} и ещё {} приложений могли быть обновлены"

View File

@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:obtainium/pages/home.dart';
import 'package:obtainium/providers/apps_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/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
@@ -118,8 +119,6 @@ void main() async {
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
var defaultThemeColour = Colors.deepPurple;
class Obtainium extends StatefulWidget {
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
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null &&
darkDynamic != null &&
settingsProvider.colour == ColourSettings.materialYou) {
if (lightDynamic != null && darkDynamic != null && settingsProvider.useMaterialYou) {
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
} else {
lightColorScheme = ColorScheme.fromSeed(seedColor: defaultThemeColour);
lightColorScheme = ColorScheme.fromSeed(seedColor: settingsProvider.themeColor);
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
@@ -231,6 +228,8 @@ class _ObtainiumState extends State<Obtainium> {
.harmonized();
}
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
return MaterialApp(
title: 'Obtainium',
localizationsDelegates: context.localizationDelegates,

View File

@@ -1,5 +1,7 @@
import 'package:device_info_plus/device_info_plus.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:obtainium/components/custom_app_bar.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:provider/provider.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';
class SettingsPage extends StatefulWidget {
@@ -22,78 +25,196 @@ class SettingsPage extends StatefulWidget {
}
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
Widget build(BuildContext context) {
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
SourceProvider sourceProvider = SourceProvider();
if (settingsProvider.prefs == null) {
settingsProvider.initializeSettings();
if (settingsProvider.prefs == null) 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(
decoration: InputDecoration(labelText: tr('installMethod')),
value: settingsProvider.installMethod,
items: [
DropdownMenuItem(
value: InstallMethodSettings.normal,
child: Text(tr('normal')),
),
const DropdownMenuItem(
value: InstallMethodSettings.shizuku,
child: Text('Shizuku'),
),
DropdownMenuItem(
value: InstallMethodSettings.root,
child: Text(tr('root')),
)
],
onChanged: (value) {
if (value != null) {
settingsProvider.installMethod = value;
var colorPicker = ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(tr('selectX', args: [tr('colour')])),
subtitle: Text("${ColorTools.nameThatColor(settingsProvider.themeColor)} "
"(${ColorTools.materialNameAndCode(settingsProvider.themeColor,
colorSwatchNameMap: colorsNameMap)})"),
trailing: ColorIndicator(
width: 40,
height: 40,
borderRadius: 20,
color: settingsProvider.themeColor,
onSelectFocus: false,
onSelect: () async {
final Color colorBeforeDialog = settingsProvider.themeColor;
if (!(await colorPickerDialog())) {
setState(() {
settingsProvider.themeColor = colorBeforeDialog;
});
}
});
}
)
);
var themeDropdown = DropdownButtonFormField(
decoration: InputDecoration(labelText: tr('theme')),
value: settingsProvider.theme,
items: [
DropdownMenuItem(
value: ThemeSettings.dark,
child: Text(tr('dark')),
),
DropdownMenuItem(
value: ThemeSettings.light,
child: Text(tr('light')),
),
DropdownMenuItem(
value: ThemeSettings.system,
child: Text(tr('followSystem')),
)
],
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 useMaterialThemeSwitch = FutureBuilder(
builder: (ctx, val) {
return ((val.data?.version.sdkInt ?? 0) >= 31) ?
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('useMaterialYou'))),
Switch(
value: settingsProvider.useMaterialYou,
onChanged: (value) {
settingsProvider.useMaterialYou = value;
})
],
) : const SizedBox.shrink();
},
future: DeviceInfoPlugin().androidInfo
);
var sortDropdown = DropdownButtonFormField(
isExpanded: true,
@@ -165,30 +286,29 @@ class _SettingsPageState extends State<SettingsPage> {
}
});
var intervalDropdown = DropdownButtonFormField(
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
value: settingsProvider.updateInterval,
items: updateIntervals.map((e) {
int displayNum = (e < 60
? e
: e < 1440
? e / 60
: e / 1440)
.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;
}
var intervalSlider = Slider(
value: settingsProvider.updateIntervalSliderVal,
max: updateIntervalNodes.length.toDouble(),
divisions: updateIntervalNodes.length * 20,
label: updateIntervalLabel,
onChanged: (double value) {
setState(() {
settingsProvider.updateIntervalSliderVal = value;
processIntervalSliderValue(value);
});
},
onChangeStart: (double value) {
setState(() {
showIntervalLabel = false;
});
},
onChangeEnd: (double value) {
setState(() {
showIntervalLabel = true;
settingsProvider.updateInterval = updateInterval;
});
},
);
var sourceSpecificFields = sourceProvider.sources.map((e) {
if (e.sourceConfigSettingFormItems.isNotEmpty) {
@@ -239,15 +359,19 @@ class _SettingsPageState extends State<SettingsPage> {
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
intervalDropdown,
//intervalDropdown,
height16,
if (showIntervalLabel) SizedBox(
child: Text("${tr('bgUpdateCheckInterval')}: $updateIntervalLabel")
) else const SizedBox(height: 16),
intervalSlider,
FutureBuilder(
builder: (ctx, val) {
return (val.data?.version.sdkInt ?? 0) >= 30
return ((val.data?.version.sdkInt ?? 0) >= 30) || settingsProvider.useShizuku
? Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
height16,
Row(
mainAxisAlignment:
MainAxisAlignment
@@ -385,38 +509,65 @@ class _SettingsPageState extends State<SettingsPage> {
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(tr(
'beforeNewInstallsShareToAppVerifier')),
GestureDetector(
onTap: () {
launchUrlString(
'https://github.com/soupslurpr/AppVerifier',
mode: LaunchMode
.externalApplication);
},
child: Text(
tr('about'),
style: const TextStyle(
decoration:
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(tr(
'beforeNewInstallsShareToAppVerifier')),
GestureDetector(
onTap: () {
launchUrlString(
'https://github.com/soupslurpr/AppVerifier',
mode: LaunchMode
.externalApplication);
},
child: Text(
tr('about'),
style: const TextStyle(
decoration:
TextDecoration.underline,
fontSize: 12),
)),
],
)),
fontSize: 12),
)),
],
)),
Switch(
value: settingsProvider
.beforeNewInstallsShareToAppVerifier,
onChanged: (value) {
settingsProvider
.beforeNewInstallsShareToAppVerifier =
.beforeNewInstallsShareToAppVerifier =
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,
Text(
tr('sourceSpecific'),
@@ -445,8 +596,9 @@ class _SettingsPageState extends State<SettingsPage> {
})
],
),
colourDropdown,
height16,
useMaterialThemeSwitch,
if (!settingsProvider.useMaterialYou) colorPicker,
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -460,34 +612,35 @@ class _SettingsPageState extends State<SettingsPage> {
),
height16,
localeDropdown,
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('useSystemFont'))),
Switch(
value: settingsProvider.useSystemFont,
onChanged: (useSystemFont) {
if (useSystemFont) {
NativeFeatures.loadSystemFont()
.then((fontLoadRes) {
if (fontLoadRes == 'ok') {
settingsProvider.useSystemFont =
true;
} else {
showError(
ObtainiumError(tr(
'systemFontError',
args: [fontLoadRes])),
context);
}
});
} else {
settingsProvider.useSystemFont = false;
}
})
],
),
FutureBuilder(
builder: (ctx, val) {
return (val.data?.version.sdkInt ?? 0) >= 34
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('useSystemFont'))),
Switch(
value: settingsProvider.useSystemFont,
onChanged: (useSystemFont) {
if (useSystemFont) {
NativeFeatures.loadSystemFont().then((val) {
settingsProvider.useSystemFont = true;
});
} else {
settingsProvider.useSystemFont = false;
}
})
]
)
]
)
: const SizedBox.shrink();
},
future: DeviceInfoPlugin().androidInfo),
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,

View File

@@ -34,7 +34,7 @@ import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:share_plus/share_plus.dart';
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();
@@ -507,9 +507,6 @@ class AppsProvider with ChangeNotifier {
.isNotEmpty;
Future<bool> canInstallSilently(App app) async {
if (app.id == obtainiumId) {
return false;
}
if (!settingsProvider.enableBackgroundUpdates) {
return false;
}
@@ -517,8 +514,7 @@ class AppsProvider with ChangeNotifier {
return false;
}
if (app.apkUrls.length > 1) {
// Manual API selection means silent install is not possible
return false;
return false; // Manual API selection means silent install is not possible
}
var osInfo = await DeviceInfoPlugin().androidInfo;
@@ -529,20 +525,29 @@ class AppsProvider with ChangeNotifier {
?.installingPackageName
: (await pm.getInstallerPackageName(packageName: app.id));
} 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;
}
int? targetSDK =
(await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
// The OS must also be new enough and the APK should target a new enough API
return osInfo.version.sdkInt >= 31 &&
targetSDK != null &&
targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
(osInfo.version.sdkInt - 3);
if (settingsProvider.useShizuku) {
return true;
}
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 {
@@ -566,7 +571,7 @@ class AppsProvider with ChangeNotifier {
Future<bool> installXApkDir(
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
// 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
@@ -581,7 +586,8 @@ class AppsProvider with ChangeNotifier {
somethingInstalled = somethingInstalled ||
await installApk(
DownloadedApk(dir.appId, file), firstTimeWithContext,
needsBGWorkaround: needsBGWorkaround);
needsBGWorkaround: needsBGWorkaround,
shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay);
} catch (e) {
logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
@@ -604,7 +610,7 @@ class AppsProvider with ChangeNotifier {
Future<bool> installApk(
DownloadedApk file, BuildContext? firstTimeWithContext,
{bool needsBGWorkaround = false}) async {
{bool needsBGWorkaround = false, bool shizukuPretendToBeGooglePlay = false}) async {
if (firstTimeWithContext != null &&
settingsProvider.beforeNewInstallsShareToAppVerifier &&
(await getInstalledInfo('dev.soupslurpr.appverifier')) != null) {
@@ -632,8 +638,7 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
if (needsBGWorkaround &&
settingsProvider.installMethod == InstallMethodSettings.normal) {
if (needsBGWorkaround) {
// 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
@@ -645,20 +650,11 @@ class AppsProvider with ChangeNotifier {
attemptToCorrectInstallStatus: false);
}
int? code;
switch (settingsProvider.installMethod) {
case InstallMethodSettings.normal:
code = await AndroidPackageInstaller.installApk(
apkFilePath: file.file.path);
case InstallMethodSettings.shizuku:
code = (await NativeFeatures.installWithShizuku(
apkFileUri: file.file.uri.toString()))
? 0
: 1;
case InstallMethodSettings.root:
code =
(await NativeFeatures.installWithRoot(apkFilePath: file.file.path))
? 0
: 1;
if (!settingsProvider.useShizuku) {
code = await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
} else {
code = await ShizukuApkInstaller.installAPK(file.file.uri.toString(),
shizukuPretendToBeGooglePlay ? "com.android.vending" : "");
}
bool installed = false;
if (code != null && code != 0 && code != 3) {
@@ -716,8 +712,8 @@ class AppsProvider with ChangeNotifier {
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
if (urlsToSelectFrom.length > 1 && context != null) {
// ignore: use_build_context_synchronously
appFileUrl = await showDialog(
// ignore: use_build_context_synchronously
context: context,
builder: (BuildContext ctx) {
return AppFilePicker(
@@ -737,10 +733,9 @@ class AppsProvider with ChangeNotifier {
if (appFileUrl != null &&
getHost(appFileUrl.value) != getHost(app.url) &&
context != null) {
// ignore: use_build_context_synchronously
if (!(settingsProvider.hideAPKOriginWarning) &&
// ignore: use_build_context_synchronously
await showDialog(
// ignore: use_build_context_synchronously
context: context,
builder: (BuildContext ctx) {
return APKOriginWarningDialog(
@@ -828,23 +823,21 @@ class AppsProvider with ChangeNotifier {
}
id = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent = await canInstallSilently(apps[id]!.app);
switch (settingsProvider.installMethod) {
case InstallMethodSettings.normal:
if (!(await settingsProvider.getInstallPermission(
enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
case InstallMethodSettings.shizuku:
int code = await NativeFeatures.checkPermissionShizuku();
if (code == -1) {
if (!settingsProvider.useShizuku) {
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
} else {
switch((await ShizukuApkInstaller.checkPermission())!){
case 'binder_not_found':
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'));
}
case InstallMethodSettings.root:
if (!(await NativeFeatures.checkPermissionRoot())) {
throw ObtainiumError(tr('cancelled'));
}
}
}
if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously
@@ -857,27 +850,32 @@ class AppsProvider with ChangeNotifier {
bool sayInstalled = true;
var contextIfNewInstall =
apps[id]?.installedInfo == null ? context : null;
bool needBGWorkaround = willBeSilent && context == null && !settingsProvider.useShizuku;
if (downloadedFile != null) {
if (willBeSilent && context == null) {
installApk(downloadedFile, contextIfNewInstall,
needsBGWorkaround: true);
if (needBGWorkaround) {
// ignore: use_build_context_synchronously
installApk(downloadedFile, contextIfNewInstall, needsBGWorkaround: true);
} else {
sayInstalled =
await installApk(downloadedFile, contextIfNewInstall);
// ignore: use_build_context_synchronously
sayInstalled = await installApk(downloadedFile, contextIfNewInstall, shizukuPretendToBeGooglePlay: apps[id]!.app.additionalSettings['shizukuPretendToBeGooglePlay'] == true);
}
} else {
if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, contextIfNewInstall,
needsBGWorkaround: true);
if (needBGWorkaround) {
// ignore: use_build_context_synchronously
installXApkDir(downloadedDir!, contextIfNewInstall, needsBGWorkaround: true);
} else {
sayInstalled =
await installXApkDir(downloadedDir!, contextIfNewInstall);
// ignore: use_build_context_synchronously
sayInstalled = await installXApkDir(downloadedDir!, contextIfNewInstall, shizukuPretendToBeGooglePlay: apps[id]!.app.additionalSettings['shizukuPretendToBeGooglePlay'] == true);
}
}
if (willBeSilent && context == null) {
notificationsProvider?.notify(SilentUpdateAttemptNotification(
[apps[id]!.app],
id: id.hashCode));
if (!settingsProvider.useShizuku) {
notificationsProvider?.notify(SilentUpdateAttemptNotification(
[apps[id]!.app], id: id.hashCode));
} else {
notificationsProvider?.notify(SilentUpdateNotification(
[apps[id]!.app], sayInstalled, id: id.hashCode));
}
}
if (sayInstalled) {
installedIds.add(id);
@@ -1710,7 +1708,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
int maxRetryWaitSeconds = 5;
var netResult = await (Connectivity().checkConnectivity());
if (netResult == ConnectivityResult.none) {
if (netResult.contains(ConnectivityResult.none)) {
logs.add('BG update task: No network.');
return;
}
@@ -1747,8 +1745,8 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
var networkRestricted = false;
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
networkRestricted = !netResult.contains(ConnectivityResult.wifi) &&
!netResult.contains(ConnectivityResult.ethernet);
}
if (toCheck.isNotEmpty) {
@@ -1792,8 +1790,8 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
var networkRestricted = false;
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
var netResult = await (Connectivity().checkConnectivity());
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
networkRestricted = !netResult.contains(ConnectivityResult.wifi) &&
!netResult.contains(ConnectivityResult.ethernet);
}
try {

View File

@@ -1,75 +1,22 @@
import 'dart:async';
import 'dart:io';
import 'package:android_system_font/android_system_font.dart';
import 'package:flutter/services.dart';
class NativeFeatures {
static const MethodChannel _channel = MethodChannel('native');
static bool _systemFontLoaded = false;
static bool _callbacksApplied = false;
static int _resPermShizuku = -2; // not set
static Future<ByteData> _readFileBytes(String path) async {
var file = File(path);
var bytes = await file.readAsBytes();
var bytes = await File(path).readAsBytes();
return ByteData.view(bytes.buffer);
}
static Future _handleCalls(MethodCall call) async {
if (call.method == 'resPermShizuku') {
_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
static Future loadSystemFont() async {
if (_systemFontLoaded) return;
var fontLoader = FontLoader('SystemFont');
fontLoader.addFont(_readFileBytes(getFontRes));
await fontLoader.load();
var fontFilePath = await AndroidSystemFont().getFilePath();
fontLoader.addFont(_readFileBytes(fontFilePath!));
fontLoader.load();
_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});
}
}

View File

@@ -41,20 +41,26 @@ class UpdateNotification extends ObtainiumNotification {
}
class SilentUpdateNotification extends ObtainiumNotification {
SilentUpdateNotification(List<App> updates, {int? id})
SilentUpdateNotification(List<App> updates, bool succeeded, {int? id})
: super(
id ?? 3,
tr('appsUpdated'),
succeeded
? tr('appsUpdated')
: tr('appsNotUpdated'),
'',
'APPS_UPDATED',
tr('appsUpdatedNotifChannel'),
tr('appsUpdatedNotifDescription'),
Importance.defaultImportance) {
message = updates.length == 1
? tr('xWasUpdatedToY',
args: [updates[0].finalName, updates[0].latestVersion])
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
args: [updates[0].finalName, (updates.length - 1).toString()]);
? tr(succeeded
? 'xWasUpdatedToY'
: 'xWasNotUpdatedToY',
args: [updates[0].finalName, updates[0].latestVersion])
: plural(succeeded
? 'xAndNMoreUpdatesInstalled'
: "xAndNMoreUpdatesFailed",
updates.length - 1, args: [updates[0].finalName, (updates.length - 1).toString()]);
}
}

View File

@@ -17,41 +17,14 @@ import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().hosts[0]}';
String obtainiumId = 'dev.imranr.obtainium';
String obtainiumUrl = 'https://github.com/ImranR98/Obtainium';
Color obtainiumThemeColor = const Color(0xFF6438B5);
enum InstallMethodSettings { normal, shizuku, root }
enum ThemeSettings { system, light, dark }
enum ColourSettings { basic, materialYou }
enum ThemeSettings { light, dark, system }
enum SortColumnSettings { added, nameAuthor, authorName, releaseDate }
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 {
SharedPreferences? prefs;
String? defaultAppDir;
@@ -75,19 +48,18 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
InstallMethodSettings get installMethod {
return InstallMethodSettings.values[
prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
bool get useShizuku{
return prefs?.getBool('useShizuku') ?? false;
}
set installMethod(InstallMethodSettings t) {
prefs?.setInt('installMethod', t.index);
set useShizuku(bool useShizuku) {
prefs?.setBool('useShizuku', useShizuku);
notifyListeners();
}
ThemeSettings get theme {
return ThemeSettings
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
.values[prefs?.getInt('theme') ?? ThemeSettings.light.index];
}
set theme(ThemeSettings t) {
@@ -95,13 +67,23 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
ColourSettings get colour {
return ColourSettings
.values[prefs?.getInt('colour') ?? ColourSettings.basic.index];
Color get themeColor {
int? colorCode = prefs?.getInt('themeColor');
return (colorCode != null) ?
Color(colorCode) : obtainiumThemeColor;
}
set colour(ColourSettings t) {
prefs?.setInt('colour', t.index);
set themeColor(Color themeColor) {
prefs?.setInt('themeColor', themeColor.value);
notifyListeners();
}
bool get useMaterialYou {
return prefs?.getBool('useMaterialYou') ?? false;
}
set useMaterialYou(bool useMaterialYou) {
prefs?.setBool('useMaterialYou', useMaterialYou);
notifyListeners();
}
@@ -115,21 +97,20 @@ class SettingsProvider with ChangeNotifier {
}
int get updateInterval {
var min = 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;
return prefs?.getInt('updateInterval') ?? 360;
}
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();
}

View File

@@ -521,6 +521,11 @@ abstract class AppSource {
label: tr('autoApkFilterByArch'), defaultValue: true)
],
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
[
GeneratedFormSwitch('shizukuPretendToBeGooglePlay',
label: tr('shizukuPretendToBeGooglePlay'),
defaultValue: false)
],
[
GeneratedFormSwitch('exemptFromBackgroundUpdates',
label: tr('exemptFromBackgroundUpdates'))

View File

@@ -26,6 +26,15 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -226,6 +235,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -266,6 +283,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description: flutter
@@ -366,6 +399,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.5"
fraction:
dependency: transitive
description:
name: fraction
sha256: "09e9504c9177bbd77df56e5d147abfbb3b43360e64bf61510059c14d6a82d524"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
gtk:
dependency: transitive
description:
@@ -634,10 +675,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "5.4.0"
platform:
dependency: transitive
description:
@@ -750,6 +791,15 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description: flutter
@@ -1007,10 +1057,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.3.0"
yaml:
dependency: transitive
description:
@@ -1020,5 +1070,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.3.0 <4.0.0"
dart: ">=3.3.3 <4.0.0"
flutter: ">=3.19.0"

View File

@@ -68,6 +68,16 @@ dependencies:
crypto: ^3.0.3
app_links: ^4.0.0
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:
flutter_test: