mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
18 Commits
v0.14.40-b
...
v0.14.41-b
Author | SHA1 | Date | |
---|---|---|---|
45fa0a165a | |||
0e5c07a078 | |||
601a742c71 | |||
c972401b6e | |||
024e81cf01 | |||
975ed402d5 | |||
b9e8083744 | |||
bb859708bc | |||
3cf2c221ac | |||
6edd7edcd2 | |||
4e26a02d78 | |||
bb36a57053 | |||
b291c800f1 | |||
375b9bce30 | |||
b6b8db48df | |||
36e6c267b9 | |||
de60c4ee9e | |||
de67e40c00 |
@ -23,6 +23,7 @@ if (flutterVersionName == null) {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'dev.rikka.tools.refine'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@ -52,8 +53,8 @@ android {
|
||||
applicationId "dev.imranr.obtainium"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@ -90,6 +91,24 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
def shizuku_version = '13.1.5'
|
||||
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
||||
implementation "dev.rikka.shizuku:provider:$shizuku_version"
|
||||
|
||||
def hidden_api_version = '4.1.0'
|
||||
// DO NOT UPDATE Hidden API without updating the Android tools
|
||||
// and do not update Android tools without updating the whole Flutter
|
||||
// (also in android/build.gradle)
|
||||
implementation "dev.rikka.tools.refine:runtime:$hidden_api_version"
|
||||
implementation "dev.rikka.hidden:compat:$hidden_api_version"
|
||||
compileOnly "dev.rikka.hidden:stub:$hidden_api_version"
|
||||
|
||||
implementation "com.github.topjohnwu.libsu:core:5.2.2"
|
||||
}
|
||||
|
@ -66,6 +66,13 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
@ -1,6 +0,0 @@
|
||||
package dev.imranr.obtainium
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
@ -0,0 +1,171 @@
|
||||
package dev.imranr.obtainium
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.IPackageInstaller
|
||||
import android.content.pm.IPackageInstallerSession
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import androidx.annotation.NonNull
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import dev.imranr.obtainium.util.IIntentSenderAdaptor
|
||||
import dev.imranr.obtainium.util.IntentSenderUtils
|
||||
import dev.imranr.obtainium.util.PackageInstallerUtils
|
||||
import dev.imranr.obtainium.util.ShizukuSystemServerApi
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private var installersChannel: MethodChannel? = null
|
||||
private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
|
||||
|
||||
private fun shizukuCheckPermission(result: Result) {
|
||||
try {
|
||||
if (Shizuku.isPreV11()) { // Unsupported
|
||||
result.success(-1)
|
||||
} else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||
result.success(1)
|
||||
} else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
|
||||
result.success(0)
|
||||
} else {
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
result.success(-2)
|
||||
}
|
||||
} catch (_: Exception) { // If shizuku not running
|
||||
result.success(-1)
|
||||
}
|
||||
}
|
||||
|
||||
private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
|
||||
requestCode: Int, grantResult: Int ->
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
|
||||
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
|
||||
}
|
||||
}
|
||||
|
||||
private fun shizukuInstallApk(apkFileUri: String, result: Result) {
|
||||
val uri = Uri.parse(apkFileUri)
|
||||
var res = false
|
||||
var session: PackageInstaller.Session? = null
|
||||
try {
|
||||
val iPackageInstaller: IPackageInstaller =
|
||||
ShizukuSystemServerApi.PackageManager_getPackageInstaller()
|
||||
val isRoot = Shizuku.getUid() == 0
|
||||
// The reason for use "com.android.shell" as installer package under adb
|
||||
// is that getMySessions will check installer package's owner
|
||||
val installerPackageName = if (isRoot) packageName else "com.android.shell"
|
||||
var installerAttributionTag: String? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
installerAttributionTag = attributionTag
|
||||
}
|
||||
val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
|
||||
val packageInstaller = PackageInstallerUtils.createPackageInstaller(
|
||||
iPackageInstaller, installerPackageName, installerAttributionTag, userId)
|
||||
val params =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
|
||||
installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
|
||||
PackageInstallerUtils.setInstallFlags(params, installFlags)
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val iSession = IPackageInstallerSession.Stub.asInterface(
|
||||
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
|
||||
session = PackageInstallerUtils.createSession(iSession)
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
val openedSession = session.openWrite("apk.apk", 0, -1)
|
||||
val buffer = ByteArray(8192)
|
||||
var length: Int
|
||||
try {
|
||||
while (inputStream!!.read(buffer).also { length = it } > 0) {
|
||||
openedSession.write(buffer, 0, length)
|
||||
openedSession.flush()
|
||||
session.fsync(openedSession)
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
inputStream!!.close()
|
||||
openedSession.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val results = arrayOf<Intent?>(null)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
val intentSender: IntentSender =
|
||||
IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() {
|
||||
override fun send(intent: Intent?) {
|
||||
results[0] = intent
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
})
|
||||
session.commit(intentSender)
|
||||
countDownLatch.await()
|
||||
res = results[0]!!.getIntExtra(
|
||||
PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
|
||||
} catch (_: Exception) {
|
||||
res = false
|
||||
} finally {
|
||||
if (session != null) {
|
||||
try {
|
||||
session.close()
|
||||
} catch (_: Exception) {
|
||||
res = false
|
||||
}
|
||||
}
|
||||
}
|
||||
result.success(res)
|
||||
}
|
||||
|
||||
private fun rootCheckPermission(result: Result) {
|
||||
Shell.getShell(Shell.GetShellCallback(
|
||||
fun(shell: Shell) {
|
||||
result.success(shell.isRoot)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private fun rootInstallApk(apkFilePath: String, result: Result) {
|
||||
Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
|
||||
val builder = StringBuilder()
|
||||
for (data in out.getOut()) { builder.append(data) }
|
||||
result.success(builder.toString().endsWith("Success"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||
installersChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, "installers")
|
||||
installersChannel!!.setMethodCallHandler {
|
||||
call, result ->
|
||||
if (call.method == "checkPermissionShizuku") {
|
||||
shizukuCheckPermission(result)
|
||||
} else if (call.method == "checkPermissionRoot") {
|
||||
rootCheckPermission(result)
|
||||
} else if (call.method == "installWithShizuku") {
|
||||
val apkFileUri: String? = call.argument("apkFileUri")
|
||||
shizukuInstallApk(apkFileUri!!, result)
|
||||
} else if (call.method == "installWithRoot") {
|
||||
val apkFilePath: String? = call.argument("apkFilePath")
|
||||
rootInstallApk(apkFilePath!!, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ApplicationUtils {
|
||||
|
||||
private static Application application;
|
||||
|
||||
public static Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public static void setApplication(Application application) {
|
||||
ApplicationUtils.application = application;
|
||||
}
|
||||
|
||||
public static String getProcessName() {
|
||||
if (Build.VERSION.SDK_INT >= 28)
|
||||
return Application.getProcessName();
|
||||
else {
|
||||
try {
|
||||
@SuppressLint("PrivateApi")
|
||||
Class<?> activityThread = Class.forName("android.app.ActivityThread");
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
Method method = activityThread.getDeclaredMethod("currentProcessName");
|
||||
return (String) method.invoke(null);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.IIntentReceiver;
|
||||
import android.content.IIntentSender;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
public abstract class IIntentSenderAdaptor extends IIntentSender.Stub {
|
||||
|
||||
public abstract void send(Intent intent);
|
||||
|
||||
@Override
|
||||
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
|
||||
send(intent);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
|
||||
send(intent);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.IIntentSender;
|
||||
import android.content.IntentSender;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class IntentSenderUtils {
|
||||
|
||||
public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
//noinspection JavaReflectionMemberAccess
|
||||
return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.IPackageInstaller;
|
||||
import android.content.pm.IPackageInstallerSession;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
@SuppressWarnings({"JavaReflectionMemberAccess"})
|
||||
public class PackageInstallerUtils {
|
||||
|
||||
public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class)
|
||||
.newInstance(installer, installerPackageName, installerAttributionTag, userId);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class)
|
||||
.newInstance(installer, installerPackageName, userId);
|
||||
} else {
|
||||
return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class)
|
||||
.newInstance(ApplicationUtils.getApplication(), ApplicationUtils.getApplication().getPackageManager(), installer, installerPackageName, userId);
|
||||
}
|
||||
}
|
||||
|
||||
public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class)
|
||||
.newInstance(session);
|
||||
|
||||
}
|
||||
|
||||
public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException {
|
||||
return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params);
|
||||
}
|
||||
|
||||
public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException {
|
||||
PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.IPackageInstaller;
|
||||
import android.content.pm.IPackageManager;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IUserManager;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import rikka.shizuku.ShizukuBinderWrapper;
|
||||
import rikka.shizuku.SystemServiceHelper;
|
||||
|
||||
public class ShizukuSystemServerApi {
|
||||
|
||||
private static final Singleton<IPackageManager> PACKAGE_MANAGER = new Singleton<IPackageManager>() {
|
||||
@Override
|
||||
protected IPackageManager create() {
|
||||
return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));
|
||||
}
|
||||
};
|
||||
|
||||
private static final Singleton<IUserManager> USER_MANAGER = new Singleton<IUserManager>() {
|
||||
@Override
|
||||
protected IUserManager create() {
|
||||
return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE)));
|
||||
}
|
||||
};
|
||||
|
||||
public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException {
|
||||
IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller();
|
||||
return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder()));
|
||||
}
|
||||
|
||||
public static List<UserInfo> UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
|
||||
} else {
|
||||
try {
|
||||
return USER_MANAGER.get().getUsers(excludeDying);
|
||||
} catch (NoSuchFieldError e) {
|
||||
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// method 2: use transactRemote directly
|
||||
/*public static List<UserInfo> UserManager_getUsers(boolean excludeDying) {
|
||||
Parcel data = SystemServiceHelper.obtainParcel(Context.USER_SERVICE, "android.os.IUserManager", "getUsers");
|
||||
Parcel reply = Parcel.obtain();
|
||||
data.writeInt(excludeDying ? 1 : 0);
|
||||
|
||||
List<UserInfo> res = null;
|
||||
try {
|
||||
ShizukuService.transactRemote(data, reply, 0);
|
||||
reply.readException();
|
||||
res = reply.createTypedArrayList(UserInfo.CREATOR);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("ShizukuSample", "UserManager#getUsers", e);
|
||||
} finally {
|
||||
data.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
return res;
|
||||
}*/
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
public abstract class Singleton<T> {
|
||||
|
||||
private T mInstance;
|
||||
|
||||
protected abstract T create();
|
||||
|
||||
public final T get() {
|
||||
synchronized (this) {
|
||||
if (mInstance == null) {
|
||||
mInstance = create();
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'dev.rikka.tools.refine:gradle-plugin:4.1.0' // Do not update!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,6 +281,11 @@
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"installMethod": "Installation method",
|
||||
"normal": "Normal",
|
||||
"shizuku": "Shizuku",
|
||||
"root": "Root",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remove App?",
|
||||
"other": "Remove Apps?"
|
||||
|
@ -11,9 +11,9 @@
|
||||
"unexpectedError": "Error Inesperado",
|
||||
"ok": "Correcto",
|
||||
"and": "y",
|
||||
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||
"githubPATLabel": "Token Github de Acceso Personal\n(Reduce tiempos de espera)",
|
||||
"includePrereleases": "Incluir versiones preliminares",
|
||||
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||
"fallbackToOlderReleases": "Retroceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
|
||||
"invalidRegEx": "Expresión inválida",
|
||||
"noDescription": "Sin descripción",
|
||||
@ -22,7 +22,7 @@
|
||||
"requiredInBrackets": "(Requerido)",
|
||||
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
|
||||
"colour": "Color",
|
||||
"githubStarredRepos": "Repositorios favoritos de GitHub",
|
||||
"githubStarredRepos": "Repositorios favoritos GitHub",
|
||||
"uname": "Nombre de usuario",
|
||||
"wrongArgNum": "Número de argumentos provistos inválido",
|
||||
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
|
||||
@ -30,7 +30,7 @@
|
||||
"app": "Aplicación",
|
||||
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
|
||||
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
|
||||
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o actalizarla.",
|
||||
"trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
|
||||
"cancelled": "Cancelado",
|
||||
"appAlreadyAdded": "Aplicación ya añadida",
|
||||
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
|
||||
@ -38,12 +38,12 @@
|
||||
"appSourceURL": "URL de Origen de la Aplicación",
|
||||
"error": "Error",
|
||||
"add": "Añadir",
|
||||
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
||||
"searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
|
||||
"search": "Buscar",
|
||||
"additionalOptsFor": "Opciones Adicionales para {}",
|
||||
"supportedSources": "Fuentes Soportadas",
|
||||
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
||||
"searchableInBrackets": "(Soporta Búsquedas)",
|
||||
"searchableInBrackets": "(soporta búsqueda)",
|
||||
"appsString": "Aplicaciones",
|
||||
"noApps": "Sin Aplicaciones",
|
||||
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
|
||||
@ -56,9 +56,9 @@
|
||||
"estimateInBrackets": "(Aproximado)",
|
||||
"selectAll": "Seleccionar Todo",
|
||||
"deselectX": "Deseleccionar {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
|
||||
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
|
||||
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
|
||||
"xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
|
||||
"removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
|
||||
"removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
|
||||
"updateX": "Actualizar {}",
|
||||
"installX": "Instalar {}",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
|
||||
@ -98,8 +98,8 @@
|
||||
"line": "Línea",
|
||||
"searchX": "Buscar {}",
|
||||
"noResults": "Resultados no encontrados",
|
||||
"importX": "Importar {}",
|
||||
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||
"importX": "Importar desde {}",
|
||||
"importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||
"importErrors": "Errores de Importación",
|
||||
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
|
||||
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
|
||||
@ -113,12 +113,12 @@
|
||||
"followSystem": "Seguir al Sistema",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Usar tema oscuro con negros puros",
|
||||
"appSortBy": "Ordenar Aplicaciones Por",
|
||||
"useBlackTheme": "Usar negros puros en tema oscuro",
|
||||
"appSortBy": "Ordenar Apps Por",
|
||||
"authorName": "Autor/Nombre",
|
||||
"nameAuthor": "Nombre/Autor",
|
||||
"asAdded": "Según se Añadieron",
|
||||
"appSortOrder": "Orden de Clasificación de Aplicaciones",
|
||||
"appSortOrder": "Orden de Clasificación",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Descendente",
|
||||
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
|
||||
@ -170,12 +170,12 @@
|
||||
"lastUpdateCheckX": "Última Comprobación: {}",
|
||||
"remove": "Eliminar",
|
||||
"yesMarkUpdated": "Sí, Marcar como Actualizada",
|
||||
"fdroid": "Repositorio oficial de F-Droid",
|
||||
"fdroid": "Repositorio oficial F-Droid",
|
||||
"appIdOrName": "ID o Nombre de la Aplicación",
|
||||
"appId": "ID de la Aplicación",
|
||||
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
|
||||
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
|
||||
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
|
||||
"fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
@ -195,8 +195,8 @@
|
||||
"category": "Categoría",
|
||||
"noCategory": "Sin Categoría",
|
||||
"noCategories": "Sin Categorías",
|
||||
"deleteCategoriesQuestion": "¿Borrar Categorías?",
|
||||
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán marcadas como 'Sin Categoría'.",
|
||||
"deleteCategoriesQuestion": "¿Eliminar Categorías?",
|
||||
"categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
|
||||
"addCategory": "Añadir Categoría",
|
||||
"label": "Nombre",
|
||||
"language": "Idioma",
|
||||
@ -211,7 +211,7 @@
|
||||
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
|
||||
"changes": "Cambios",
|
||||
"releaseDate": "Fecha de Publicación",
|
||||
"importFromURLsInFile": "Importar de URls desde un Archivo (como OPML)",
|
||||
"importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
|
||||
"versionDetection": "Detección de Versiones",
|
||||
"standardVersionDetection": "Detección de versiones estándar",
|
||||
"groupByCategory": "Agrupar por Categoría",
|
||||
@ -219,8 +219,8 @@
|
||||
"overrideSource": "Sobrescribir Fuente",
|
||||
"dontShowAgain": "No mostrar de nuevo",
|
||||
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
|
||||
"moveNonInstalledAppsToBottom": "Mover las Apps no instaladas al final de la vista de Apps",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APKs",
|
||||
"moveNonInstalledAppsToBottom": "Mover Apps no instaladas en la Parte Inferior de la Vista de Aplicaciones",
|
||||
"gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
|
||||
"about": "Acerca",
|
||||
"requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
|
||||
@ -230,7 +230,7 @@
|
||||
"pickHighestVersionCode": "Auto selección versión superior del código APK",
|
||||
"checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
|
||||
"disablePageTransitions": "Deshabilitar animaciones de transición de la página",
|
||||
"reversePageTransitions": "Invertir las animaciones de transición de la página",
|
||||
"reversePageTransitions": "Invertir animaciones de transición de la página",
|
||||
"minStarCount": "Número Mínimo de Estrellas",
|
||||
"addInfoBelow": "Añadir esta información debajo.",
|
||||
"addInfoInSettings": "Añadir esta información en Ajustes.",
|
||||
@ -244,25 +244,25 @@
|
||||
"xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
|
||||
"enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
|
||||
"backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
|
||||
"backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede verificar con Obtainium abierto.",
|
||||
"verifyLatestTag": "Verifica la etiqueta 'latest'",
|
||||
"backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
|
||||
"verifyLatestTag": "Comprueba la etiqueta 'latest'",
|
||||
"intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
|
||||
"intermediateLinkNotFound": "Enlace Intermedio no encontrado",
|
||||
"exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
|
||||
"bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
|
||||
"versionExtractionRegEx": "Versión de Extracción de RegEx",
|
||||
"matchGroupToUse": "Match Group to Use",
|
||||
"matchGroupToUse": "Coincidir en Grupo a Usar",
|
||||
"highlightTouchTargets": "Resaltar objetivos menos obvios",
|
||||
"pickExportDir": "Selecciona el Directorio para Exportar",
|
||||
"autoExportOnChanges": "Auto Exportar cuando haya cambios",
|
||||
"includeSettings": "Include settings",
|
||||
"includeSettings": "Incluir ajustes",
|
||||
"filterVersionsByRegEx": "Filtrar por Versiones",
|
||||
"trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
|
||||
"dontSortReleasesList": "Mantener el order de publicación desde API",
|
||||
"reverseSort": "Orden inverso",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"takeFirstLink": "Usar primer enlace",
|
||||
"skipSort": "Omitir orden",
|
||||
"debugMenu": "Menu Depurar",
|
||||
"bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
|
||||
"runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
|
||||
@ -277,10 +277,10 @@
|
||||
"downloadingXNotifChannel": "Descargando {}",
|
||||
"completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
|
||||
"checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas y en seguimiento",
|
||||
"supportFixedAPKURL": "Soporte para URLs fijas de APK",
|
||||
"selectX": "Selecciona {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"parallelDownloads": "Permitir descargas paralelas",
|
||||
"removeAppQuestion": {
|
||||
"one": "¿Eliminar Aplicación?",
|
||||
"other": "¿Eliminar Aplicaciones?"
|
||||
@ -318,8 +318,8 @@
|
||||
"other": "{} Días"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
|
||||
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||
"one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
|
||||
"other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} y 1 aplicación más tiene actualizaciones.",
|
||||
|
@ -277,10 +277,15 @@
|
||||
"downloadingXNotifChannel": "Загрузка {}",
|
||||
"completeAppInstallationNotifChannel": "Завершение установки приложения",
|
||||
"checkingForUpdatesNotifChannel": "Проверка обновлений",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
|
||||
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
||||
"selectX": "Выбрать {}",
|
||||
"parallelDownloads": "Разрешить параллельные загрузки",
|
||||
"installMethod": "Метод установки",
|
||||
"normal": "Нормальный",
|
||||
"shizuku": "Shizuku",
|
||||
"root": "Суперпользователь",
|
||||
"shizukuBinderNotFound": "Shizuku не запущен",
|
||||
"removeAppQuestion": {
|
||||
"one": "Удалить приложение?",
|
||||
"other": "Удалить приложения?"
|
||||
|
@ -95,7 +95,7 @@ class HTML extends AppSource {
|
||||
label: tr('sortByFileNamesNotLinks'))
|
||||
],
|
||||
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
||||
[GeneratedFormSwitch('reverseSort', label: tr('takeTopLink'))],
|
||||
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
||||
[
|
||||
GeneratedFormSwitch('supportFixedAPKURL',
|
||||
defaultValue: true, label: tr('supportFixedAPKURL')),
|
||||
|
@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:easy_localization/src/localization.dart';
|
||||
|
||||
const String currentVersion = '0.14.40';
|
||||
const String currentVersion = '0.14.41';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
|
@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
settingsProvider.initializeSettings();
|
||||
}
|
||||
|
||||
var installMethodDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('installMethod')),
|
||||
value: settingsProvider.installMethod,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.normal,
|
||||
child: Text(tr('normal')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.shizuku,
|
||||
child: Text(tr('shizuku')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.root,
|
||||
child: Text(tr('root')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.installMethod = value;
|
||||
}
|
||||
});
|
||||
|
||||
var themeDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('theme')),
|
||||
value: settingsProvider.theme,
|
||||
@ -328,6 +351,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
],
|
||||
),
|
||||
height16,
|
||||
installMethodDropdown,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -33,6 +33,7 @@ import 'package:http/http.dart';
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:flutter_archive/flutter_archive.dart';
|
||||
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||
import 'installers_provider.dart';
|
||||
|
||||
final pm = AndroidPackageManager();
|
||||
|
||||
@ -504,7 +505,8 @@ class AppsProvider with ChangeNotifier {
|
||||
!(await canDowngradeApps())) {
|
||||
throw DowngradeError();
|
||||
}
|
||||
if (needsBGWorkaround) {
|
||||
if (needsBGWorkaround &&
|
||||
settingsProvider.installMethod == InstallMethodSettings.normal) {
|
||||
// The below 'await' will never return if we are in a background process
|
||||
// To work around this, we should assume the install will be successful
|
||||
// So we update the app's installed version first as we will never get to the later code
|
||||
@ -515,8 +517,21 @@ class AppsProvider with ChangeNotifier {
|
||||
await saveApps([apps[file.appId]!.app],
|
||||
attemptToCorrectInstallStatus: false);
|
||||
}
|
||||
int? code =
|
||||
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||
int? code;
|
||||
switch (settingsProvider.installMethod) {
|
||||
case InstallMethodSettings.normal:
|
||||
code = await AndroidPackageInstaller.installApk(
|
||||
apkFilePath: file.file.path);
|
||||
case InstallMethodSettings.shizuku:
|
||||
code = (await Installers.installWithShizuku(
|
||||
apkFileUri: file.file.uri.toString()))
|
||||
? 0
|
||||
: 1;
|
||||
case InstallMethodSettings.root:
|
||||
code = (await Installers.installWithRoot(apkFilePath: file.file.path))
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
bool installed = false;
|
||||
if (code != null && code != 0 && code != 3) {
|
||||
throw InstallError(code);
|
||||
@ -672,8 +687,23 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
|
||||
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
switch (settingsProvider.installMethod) {
|
||||
case InstallMethodSettings.normal:
|
||||
if (!(await settingsProvider.getInstallPermission(
|
||||
enforce: false))) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
case InstallMethodSettings.shizuku:
|
||||
int code = await Installers.checkPermissionShizuku();
|
||||
if (code == -1) {
|
||||
throw ObtainiumError(tr('shizukuBinderNotFound'));
|
||||
} else if (code == 0) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
case InstallMethodSettings.root:
|
||||
if (!(await Installers.checkPermissionRoot())) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
}
|
||||
if (!willBeSilent && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -720,7 +750,9 @@ class AppsProvider with ChangeNotifier {
|
||||
await Future.wait(
|
||||
appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
|
||||
for (var id in appsToInstall) {
|
||||
await updateFn(id);
|
||||
if (!errors.appIdNames.containsKey(id)) {
|
||||
await updateFn(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
56
lib/providers/installers_provider.dart
Normal file
56
lib/providers/installers_provider.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Installers {
|
||||
static const MethodChannel _channel = MethodChannel('installers');
|
||||
static bool _callbacksApplied = false;
|
||||
static int _resPermShizuku = -2; // not set
|
||||
|
||||
static Future waitWhile(bool Function() test,
|
||||
[Duration pollInterval = const Duration(milliseconds: 250)]) {
|
||||
var completer = Completer();
|
||||
check() {
|
||||
if (test()) {
|
||||
Timer(pollInterval, check);
|
||||
} else {
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
check();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future handleCalls(MethodCall call) async {
|
||||
if (call.method == 'resPermShizuku') {
|
||||
_resPermShizuku = call.arguments['res'];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> checkPermissionShizuku() async {
|
||||
if (!_callbacksApplied) {
|
||||
_channel.setMethodCallHandler(handleCalls);
|
||||
_callbacksApplied = true;
|
||||
}
|
||||
int res = await _channel.invokeMethod('checkPermissionShizuku');
|
||||
if(res == -2) {
|
||||
await waitWhile(() => _resPermShizuku == -2);
|
||||
res = _resPermShizuku;
|
||||
_resPermShizuku = -2;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static Future<bool> checkPermissionRoot() async {
|
||||
return await _channel.invokeMethod('checkPermissionRoot');
|
||||
}
|
||||
|
||||
static Future<bool> installWithShizuku({required String apkFileUri}) async {
|
||||
return await _channel.invokeMethod(
|
||||
'installWithShizuku', {'apkFileUri': apkFileUri});
|
||||
}
|
||||
|
||||
static Future<bool> installWithRoot({required String apkFilePath}) async {
|
||||
return await _channel.invokeMethod(
|
||||
'installWithRoot', {'apkFilePath': apkFilePath});
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
|
||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||
String obtainiumId = 'dev.imranr.obtainium';
|
||||
|
||||
enum InstallMethodSettings { normal, shizuku, root }
|
||||
|
||||
enum ThemeSettings { system, light, dark }
|
||||
|
||||
enum ColourSettings { basic, materialYou }
|
||||
@ -49,6 +51,16 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
InstallMethodSettings get installMethod {
|
||||
return InstallMethodSettings
|
||||
.values[prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
|
||||
}
|
||||
|
||||
set installMethod(InstallMethodSettings t) {
|
||||
prefs?.setInt('installMethod', t.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ThemeSettings get theme {
|
||||
return ThemeSettings
|
||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
||||
|
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.14.40+234 # When changing this, update the tag in main() accordingly
|
||||
version: 0.14.41+235 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
Reference in New Issue
Block a user