6.7.0 - Alpha13 - 打包应用页面默认勾选必要权限 (issue #397); 配置文件键名增加修改保护并支持冲突检测
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$data": {
|
||||
"v6.7.0": {
|
||||
"released_date": "2026/01/03",
|
||||
"released_date": "2026/01/06",
|
||||
"feature": [
|
||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮)",
|
||||
"cvt 模块, 用于数据单位转换 (参阅 项目文档 > [单位转换](https://docs.autojs6.com/#/cvt))",
|
||||
@@ -65,7 +65,8 @@
|
||||
"运行项目时 project.json 配置参数可能无法正常解析的问题",
|
||||
"项目打包时 project.json 的 excludedDirs 配置参数将导致配置文件解析失败的问题 _[`issue #428`](http://issues.autojs6.com/428)_",
|
||||
"Android 7.x 可能无法正常使用打包功能的问题",
|
||||
"项目配置文件中构建版本号或构建时间出现较大数字时可能导致应用崩溃的问题",
|
||||
"脚本项目配置文件保存时原始键名可能会被修改的问题",
|
||||
"脚本项目配置文件中构建版本号或构建时间出现较大数字时可能导致应用崩溃的问题",
|
||||
"频繁获取或重建 ImageReader 时可能因缓冲区暂无可用帧导致应用崩溃的问题",
|
||||
"输入事件观察器 InputEventObserver 可能导致应用启动时明显卡顿的问题",
|
||||
"Shizuku 用户服务进程未能正常结束导致进程堆积的问题 _[`issue #474`](http://issues.autojs6.com/474)_",
|
||||
@@ -94,6 +95,8 @@
|
||||
"控制台浮动窗口背景色彩行为相关 API (透明度/着色/基色) 更符合安卓设计规范 _[`issue #458`](http://issues.autojs6.com/458)_",
|
||||
"文件管理器浮动按钮展开后点击菜单项时优化菜单收起时机",
|
||||
"文件管理器/任务面板支持显示文件/任务数量统计信息",
|
||||
"打包应用页面默认勾选必要权限 (WAKE_LOCK/INTERNET/WRITE_EXTERNAL_STORAGE) _[`issue #397`](http://issues.autojs6.com/397)_",
|
||||
"脚本项目配置文件保存时增加键名冲突检测机制防止键名歧义",
|
||||
"崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮",
|
||||
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
|
||||
"使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager",
|
||||
@@ -101,7 +104,7 @@
|
||||
"Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")",
|
||||
"Gradle 构建脚本支持自动生成 VersionCodesList 类所需数据以降低脚本启动延迟",
|
||||
"使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本",
|
||||
"模块化 Gradle 构建脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件",
|
||||
"模块化 Gradle 构建脚本, 将共享构建逻辑迁移至 build-logic 并抽象为约定插件",
|
||||
"使用 Gradle 约定插件简化本地 AAR 库加载逻辑",
|
||||
"使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异"
|
||||
],
|
||||
|
||||
@@ -208,7 +208,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
|
||||
ProjectConfig.fromProjectDir(config.sourcePath)?.let { sourceProjectConfig ->
|
||||
sourceProjectConfig
|
||||
.setBuildInfo(BuildInfo.generate(sourceProjectConfig.buildInfo.buildNumber + 1))
|
||||
File(ProjectConfig.configFileOfDir(config.sourcePath)).writeText(sourceProjectConfig.toJson())
|
||||
File(ProjectConfig.configFileOfDir(config.sourcePath)).writeText(sourceProjectConfig.toJson(true))
|
||||
return@run sourceProjectConfig
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
|
||||
.setBuildInfo(BuildInfo.generate(newProjectConfig.versionCode.toLong()))
|
||||
File(buildPath, "assets/project/$CONFIG_FILE_NAME").also { file ->
|
||||
file.parentFile?.let { parent -> if (!parent.exists()) parent.mkdirs() }
|
||||
}.writeText(newProjectConfig.toJson())
|
||||
}.writeText(newProjectConfig.toJson(true))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ProjectTemplate {
|
||||
public Observable<File> newProject() {
|
||||
return Observable.fromCallable(() -> {
|
||||
mProjectDir.mkdirs();
|
||||
PFiles.write(ProjectConfig.configFileOfDir(mProjectDir.getPath()), mProjectConfig.toJson());
|
||||
PFiles.write(ProjectConfig.configFileOfDir(mProjectDir.getPath()), mProjectConfig.toJson(true));
|
||||
new File(mProjectDir, mProjectConfig.getMainScriptFileName()).createNewFile();
|
||||
return mProjectDir;
|
||||
})
|
||||
|
||||
161
app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt
Normal file
161
app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt
Normal file
@@ -0,0 +1,161 @@
|
||||
package org.autojs.autojs.project
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.autojs.autojs.annotation.DeserializedMethodName
|
||||
import org.autojs.autojs.annotation.SerializedNameCompatible
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A custom deserializer for JSON objects that enables flexible data mapping, including
|
||||
* support for alternate key matching, reverse logic for booleans, and handling of custom
|
||||
* field deserialization methods. It also provides functionality for detecting conflicts
|
||||
* when multiple keys match the same field.
|
||||
*
|
||||
* zh-CN:
|
||||
*
|
||||
* 一个用于 JSON 对象的自定义反序列化器, 支持灵活的数据映射,
|
||||
* 包括 [备用键匹配/布尔值反向逻辑/自定义字段反序列化方法处理/多键匹配同一字段时检测冲突] 的功能.
|
||||
*
|
||||
* @param T
|
||||
* The type of the object being deserialized.
|
||||
* zh-CN: 待反序列化对象类型.
|
||||
*
|
||||
* @property detectConflicts
|
||||
* Whether to detect conflicting alias keys for the same field.
|
||||
* zh-CN: 是否检测同一字段的别名 key 冲突.
|
||||
*/
|
||||
class FuzzyDeserializer<T> @JvmOverloads constructor(
|
||||
private val detectConflicts: Boolean = false,
|
||||
) : JsonDeserializer<T> {
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T {
|
||||
require(typeOfT is Class<*>) {
|
||||
"Expected parameter typeOfT to be of type Class, but got: ${typeOfT?.javaClass?.name}"
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val clazz = typeOfT as Class<T>
|
||||
val instance = try {
|
||||
clazz.getDeclaredConstructor().newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Failed to create an instance of ${clazz.name}", e)
|
||||
}
|
||||
if (json is JsonObject) {
|
||||
clazz.declaredFields.forEach { processField(it, json, instance, context, detectConflicts) }
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
private fun processField(
|
||||
field: Field,
|
||||
json: JsonObject,
|
||||
instance: T,
|
||||
context: JsonDeserializationContext?,
|
||||
detectConflicts: Boolean,
|
||||
) {
|
||||
field.isAccessible = true
|
||||
|
||||
val serializedNameAnnotation = field.getAnnotation(SerializedName::class.java)
|
||||
val compatibleAnnotation = field.getAnnotation(SerializedNameCompatible::class.java)
|
||||
val deserializedAnnotation = field.getAnnotation(DeserializedMethodName::class.java)
|
||||
|
||||
/**
|
||||
* Use the primary @SerializedName value (or field name) as the canonical key for serialization.
|
||||
* zh-CN: 使用 @SerializedName 的主值 (或字段名) 作为写回 JSON 时的 canonical key.
|
||||
*/
|
||||
val canonicalKey = serializedNameAnnotation?.value ?: field.name
|
||||
|
||||
val sanitizedPrimaryName = sanitizeKey(canonicalKey)
|
||||
val sanitizedAlternateNames = serializedNameAnnotation?.alternate?.map { sanitizeKey(it) } ?: emptyList()
|
||||
val sanitizedCompatibleNames = compatibleAnnotation?.with?.map { sanitizeKey(it.value) to it.isReversed } ?: emptyList()
|
||||
|
||||
val serializedNames: List<Pair<String, Boolean>> =
|
||||
(sanitizedAlternateNames + sanitizedPrimaryName).map { it to false } + sanitizedCompatibleNames
|
||||
|
||||
// Collect all matched JSON keys for this field to detect conflicts.
|
||||
// zh-CN: 收集该字段命中的全部 JSON key, 用于检测冲突.
|
||||
val matchedJsonEntries = json.entrySet()
|
||||
.mapNotNull { (jsonKey, jsonValue) ->
|
||||
val sanitizedJsonKey = sanitizeKey(jsonKey)
|
||||
val matched = serializedNames.firstOrNull { (serializedKey, _) -> sanitizedJsonKey == serializedKey }
|
||||
matched?.let { Triple(jsonKey, jsonValue, it.second) }
|
||||
}
|
||||
|
||||
if (detectConflicts && matchedJsonEntries.size > 1) {
|
||||
val keys = matchedJsonEntries.joinToString(", ") { "\"${it.first}\"" }
|
||||
throw IllegalArgumentException("Conflicting keys for \"$canonicalKey\": $keys")
|
||||
}
|
||||
|
||||
val matched = matchedJsonEntries.lastOrNull() ?: return
|
||||
val (jsonKey, jsonValue, isReversed) = matched
|
||||
|
||||
// Record the original key for later serialization if the target supports it.
|
||||
// zh-CN: 如果目标对象支持记录, 则记录原始 key, 用于后续写回时保留原 key.
|
||||
if (instance is OriginalJsonKeyAware) {
|
||||
instance.recordOriginalJsonKey(canonicalKey, jsonKey)
|
||||
}
|
||||
|
||||
when {
|
||||
deserializedAnnotation != null -> {
|
||||
handleDeserializedMethod(field, instance, jsonValue, deserializedAnnotation)
|
||||
}
|
||||
field.type == Boolean::class.javaPrimitiveType || field.type == Boolean::class.java -> {
|
||||
if (jsonValue.isJsonPrimitive && jsonValue.asJsonPrimitive.isBoolean) {
|
||||
val value = jsonValue.asBoolean
|
||||
field.set(instance, if (isReversed) !value else value)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!jsonValue.isJsonNull) {
|
||||
val value = context?.deserialize<Any>(jsonValue, field.genericType)
|
||||
field.set(instance, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeserializedMethod(field: Field, instance: T, jsonValue: JsonElement, annotation: DeserializedMethodName) {
|
||||
require(jsonValue.isJsonPrimitive && jsonValue.asJsonPrimitive.isString) {
|
||||
"Field with @DeserializedMethodName must map to a JSON string."
|
||||
}
|
||||
val methodName = annotation.method
|
||||
val methodInput = jsonValue.asString
|
||||
val parameterTypes = annotation.parameterTypes.map { it.java }.toTypedArray()
|
||||
val method = runCatching {
|
||||
instance!!::class.java.getMethod(methodName, *parameterTypes)
|
||||
}.getOrElse { e ->
|
||||
throw RuntimeException("Method $methodName with parameter types ${parameterTypes.contentToString()} not found in ${instance!!::class.java.name}", e)
|
||||
}
|
||||
try {
|
||||
val isStatic = Modifier.isStatic(method.modifiers)
|
||||
val result = method.invoke(instance.takeUnless { isStatic }, methodInput)
|
||||
field.set(instance, result)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Failed to invoke method $methodName on ${field.name}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeKey(key: String): String {
|
||||
return key.replace(Regex("[^a-zA-Z0-9]"), "").lowercase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a hook to record which original JSON key was used to populate a field.
|
||||
* zh-CN: 提供一个钩子, 用于记录某个字段在反序列化时实际命中的 JSON key.
|
||||
*/
|
||||
interface OriginalJsonKeyAware {
|
||||
|
||||
/**
|
||||
* Record the original JSON key used for a canonical key.
|
||||
* zh-CN: 记录 canonical key 对应的原始 JSON key.
|
||||
*/
|
||||
fun recordOriginalJsonKey(canonicalKey: String, originalKey: String)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.Strictness;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.autojs.autojs.annotation.DeserializedMethodName;
|
||||
@@ -28,7 +30,9 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -38,21 +42,36 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Created by Stardust on Jan 24, 2018.
|
||||
* Modified by SuperMonster003 as of Nov 19, 2025.
|
||||
* Modified by SuperMonster003 as of Jan 6, 2026.
|
||||
*/
|
||||
public class ProjectConfig {
|
||||
public class ProjectConfig implements FuzzyDeserializer.OriginalJsonKeyAware {
|
||||
|
||||
public static final String CONFIG_FILE_NAME = "project.json";
|
||||
public static final String DEFAULT_MAIN_SCRIPT_FILE_NAME = "main.js";
|
||||
|
||||
private static final String TAG = "ProjectConfig";
|
||||
|
||||
public static final List<String> DEFAULT_PERMISSIONS = Arrays.asList(
|
||||
"android.permission.WAKE_LOCK",
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
);
|
||||
|
||||
private static final Gson sGson = new GsonBuilder()
|
||||
.registerTypeAdapter(ProjectConfig.class, new JsonUtils.FuzzyDeserializer<ProjectConfig>())
|
||||
.registerTypeAdapter(LaunchConfig.class, new JsonUtils.FuzzyDeserializer<LaunchConfig>())
|
||||
.registerTypeAdapter(ScriptConfig.class, new JsonUtils.FuzzyDeserializer<ScriptConfig>())
|
||||
.registerTypeAdapter(BuildInfo.class, new JsonUtils.FuzzyDeserializer<BuildInfo>())
|
||||
.registerTypeAdapter(ProjectConfig.class, new FuzzyDeserializer<ProjectConfig>())
|
||||
.registerTypeAdapter(LaunchConfig.class, new FuzzyDeserializer<LaunchConfig>())
|
||||
.registerTypeAdapter(ScriptConfig.class, new FuzzyDeserializer<ScriptConfig>())
|
||||
.registerTypeAdapter(BuildInfo.class, new FuzzyDeserializer<BuildInfo>())
|
||||
.setStrictness(Strictness.LENIENT)
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* Remember original JSON keys to preserve them on serialization (canonicalKey -> originalKey).
|
||||
* zh-CN: 记录原始 JSON key, 用于序列化写回时保留原 key (canonicalKey -> originalKey).
|
||||
*/
|
||||
private final transient Map<String, String> mOriginalJsonKeys = new LinkedHashMap<>();
|
||||
|
||||
@SerializedName("excludeDirs")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "ignoredDirs", target = {"AutoJs4", "AutoX"}),
|
||||
@@ -60,23 +79,28 @@ public class ProjectConfig {
|
||||
@With(value = "excludedDirs"),
|
||||
})
|
||||
private final List<String> mExcludedDirs = new ArrayList<>();
|
||||
|
||||
@SerializedName("name")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "projectName"),
|
||||
})
|
||||
private String mName;
|
||||
|
||||
@SerializedName("versionName")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "version"),
|
||||
})
|
||||
private String mVersionName;
|
||||
|
||||
@SerializedName("versionCode")
|
||||
private int mVersionCode = 1;
|
||||
|
||||
@SerializedName("packageName")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "package"),
|
||||
})
|
||||
private String mPackageName;
|
||||
|
||||
@SerializedName("main")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "mainName"),
|
||||
@@ -88,6 +112,7 @@ public class ProjectConfig {
|
||||
@With(value = "mainFileName"),
|
||||
})
|
||||
private String mMainScriptFileName = DEFAULT_MAIN_SCRIPT_FILE_NAME;
|
||||
|
||||
@Nullable
|
||||
@SerializedName(value = "assets")
|
||||
@SerializedNameCompatible(with = {
|
||||
@@ -95,22 +120,27 @@ public class ProjectConfig {
|
||||
@With(value = "assetList"),
|
||||
})
|
||||
private List<String> mAssets = new ArrayList<>();
|
||||
|
||||
@SerializedName("launchConfig")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "launch"),
|
||||
})
|
||||
private LaunchConfig mLaunchConfig = new LaunchConfig();
|
||||
|
||||
@SerializedName("build")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "buildInfo"),
|
||||
})
|
||||
private BuildInfo mBuildInfo = new BuildInfo();
|
||||
|
||||
@SerializedName("icon")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "iconPath"),
|
||||
})
|
||||
private String mIconPath;
|
||||
|
||||
private transient Callable<Bitmap> mIconBitmapGetter;
|
||||
|
||||
@Nullable
|
||||
@SerializedName(value = "abis")
|
||||
@SerializedNameCompatible(with = {
|
||||
@@ -118,6 +148,7 @@ public class ProjectConfig {
|
||||
@With(value = "abiList"),
|
||||
})
|
||||
private List<String> mAbis = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
@SerializedName(value = "libs")
|
||||
@SerializedNameCompatible(with = {
|
||||
@@ -125,12 +156,14 @@ public class ProjectConfig {
|
||||
@With(value = "libList"),
|
||||
})
|
||||
private List<String> mLibs = new ArrayList<>();
|
||||
|
||||
@SerializedName("permissions")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "permission"),
|
||||
@With(value = "permissionList"),
|
||||
})
|
||||
private List<String> mPermissions = new ArrayList<>();
|
||||
private List<String> mPermissions = new ArrayList<>(DEFAULT_PERMISSIONS);
|
||||
|
||||
@SerializedName("signatureScheme")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "signatureSchemes"),
|
||||
@@ -150,8 +183,10 @@ public class ProjectConfig {
|
||||
// # @With(value = "scripts", target = {"AutoJs4", "AutoX"}),
|
||||
// # })
|
||||
// # private final Map<String, ScriptConfig> mScriptConfigs = new HashMap<>();
|
||||
|
||||
@Nullable
|
||||
private transient KeyStore mKeyStore = null;
|
||||
|
||||
@SerializedName(value = "useFeatures")
|
||||
@SerializedNameCompatible(with = {
|
||||
@With(value = "useFeature"),
|
||||
@@ -161,6 +196,7 @@ public class ProjectConfig {
|
||||
@With(value = "featureList"),
|
||||
})
|
||||
private List<String> mFeatures = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
private transient String mSourcePath = null;
|
||||
|
||||
@@ -224,7 +260,7 @@ public class ProjectConfig {
|
||||
} catch (Exception e2) {
|
||||
Log.d(TAG, "Failed to read repaired json from file: " + path + " (" + e2.getMessage() + ")");
|
||||
try {
|
||||
ProjectConfig crucialData = tryReadCrucialData(fileContents, path);
|
||||
ProjectConfig crucialData = tryReadCrucialData(fileContents, path, false);
|
||||
Log.d(TAG, "Successfully read crucial data from file: " + path);
|
||||
return crucialData;
|
||||
} catch (Exception e3) {
|
||||
@@ -235,54 +271,67 @@ public class ProjectConfig {
|
||||
}
|
||||
}
|
||||
|
||||
private static ProjectConfig tryReadCrucialData(String s, String jsonFilePath) {
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static ProjectConfig tryReadCrucialData(String s, String jsonFilePath, boolean detectConflicts) {
|
||||
ProjectConfig projectConfig = new ProjectConfig();
|
||||
LaunchConfig launchConfig = new LaunchConfig();
|
||||
BuildInfo buildInfo = new BuildInfo();
|
||||
|
||||
Pattern namePattern = Pattern.compile(stringPattern("name"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern versionNamePattern = Pattern.compile(stringPattern("versionName"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern versionCodePattern = Pattern.compile(numberPattern("versionCode"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern packageNamePattern = Pattern.compile(stringPattern("packageName"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern mainPattern = Pattern.compile(stringPattern("main"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern iconPattern = Pattern.compile(stringPattern("icon"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern namePattern = Pattern.compile(stringPatternWithKeyCapture("name"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern versionNamePattern = Pattern.compile(stringPatternWithKeyCapture("versionName"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern versionCodePattern = Pattern.compile(numberPatternWithKeyCapture("versionCode"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern packageNamePattern = Pattern.compile(stringPatternWithKeyCapture("packageName"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern mainPattern = Pattern.compile(stringPatternWithKeyCapture("main"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern iconPattern = Pattern.compile(stringPatternWithKeyCapture("icon"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Pattern assetsPattern = Pattern.compile(listPattern("asset"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern abisPattern = Pattern.compile(listPattern("abi"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern libsPattern = Pattern.compile(listPattern("lib"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern useFeaturesPattern = Pattern.compile(listPattern("useFeature"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern assetsPattern = Pattern.compile(listPatternWithKeyCapture("asset"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern abisPattern = Pattern.compile(listPatternWithKeyCapture("abi"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern libsPattern = Pattern.compile(listPatternWithKeyCapture("lib"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern useFeaturesPattern = Pattern.compile(listPatternWithKeyCapture("useFeature"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Pattern buildTimePattern = Pattern.compile(numberPattern("buildTime"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern buildNumberPattern = Pattern.compile(numberPattern("buildNumber"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern buildIdPattern = Pattern.compile(stringPattern("buildId"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern permissionsPattern = Pattern.compile(listPatternWithKeyCapture("permission"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern signatureSchemePattern = Pattern.compile(stringPatternWithKeyCapture("signatureScheme"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Pattern launchConfigHideLogsPattern = Pattern.compile(booleanPattern("hideLogs"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern launchConfigLogsVisiblePattern = Pattern.compile(booleanPattern("logsVisible"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern excludeDirsPattern = Pattern.compile(listPatternWithKeyCapture("excludeDir"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern excludedDirsPattern = Pattern.compile(listPatternWithKeyCapture("excludedDir"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Pattern launchConfigDisplaySplashPattern = Pattern.compile(booleanPattern("displaySplash"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern launchConfigSplashVisiblePattern = Pattern.compile(booleanPattern("splashVisible"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern buildTimePattern = Pattern.compile(numberPatternWithKeyCapture("buildTime"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern buildNumberPattern = Pattern.compile(numberPatternWithKeyCapture("buildNumber"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern buildIdPattern = Pattern.compile(stringPatternWithKeyCapture("buildId"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
setFieldIfMatches(namePattern, s, projectConfig::setName);
|
||||
setFieldIfMatches(versionNamePattern, s, projectConfig::setVersionName);
|
||||
setFieldForDoubleIfMatches(versionCodePattern, s, versionCode -> projectConfig.setVersionCode((int) versionCode));
|
||||
setFieldIfMatches(packageNamePattern, s, projectConfig::setPackageName);
|
||||
setFieldIfMatches(mainPattern, s, projectConfig::setMainScriptFileName);
|
||||
setFieldIfMatches(iconPattern, s, projectConfig::setIconPath);
|
||||
Pattern launchConfigHideLogsPattern = Pattern.compile(booleanPatternWithKeyCapture("hideLogs"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern launchConfigLogsVisiblePattern = Pattern.compile(booleanPatternWithKeyCapture("logsVisible"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
setListIfMatches(assetsPattern, s, projectConfig::setAssets);
|
||||
setListIfMatches(abisPattern, s, projectConfig::setAbis);
|
||||
setListIfMatches(libsPattern, s, projectConfig::setLibs);
|
||||
setListIfMatches(useFeaturesPattern, s, projectConfig::setFeatures);
|
||||
Pattern launchConfigDisplaySplashPattern = Pattern.compile(booleanPatternWithKeyCapture("displaySplash"), Pattern.CASE_INSENSITIVE);
|
||||
Pattern launchConfigSplashVisiblePattern = Pattern.compile(booleanPatternWithKeyCapture("splashVisible"), Pattern.CASE_INSENSITIVE);
|
||||
|
||||
setFieldForDoubleIfMatches(buildTimePattern, s, buildTime -> buildInfo.setBuildTime((long) buildTime));
|
||||
setFieldForDoubleIfMatches(buildNumberPattern, s, buildNumber -> buildInfo.setBuildNumber((long) buildNumber));
|
||||
setFieldIfMatches(buildIdPattern, s, buildInfo::setBuildId);
|
||||
setFieldIfMatchesWithKey(namePattern, s, "name", projectConfig::setName, projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(versionNamePattern, s, "versionName", projectConfig::setVersionName, projectConfig, detectConflicts);
|
||||
setFieldForDoubleIfMatchesWithKey(versionCodePattern, s, "versionCode", versionCode -> projectConfig.setVersionCode((int) versionCode), projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(packageNamePattern, s, "packageName", projectConfig::setPackageName, projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(mainPattern, s, "main", projectConfig::setMainScriptFileName, projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(iconPattern, s, "icon", projectConfig::setIconPath, projectConfig, detectConflicts);
|
||||
|
||||
setFieldForBooleanIfMatches(launchConfigHideLogsPattern, s, value -> launchConfig.setLogsVisible(!value));
|
||||
setFieldForBooleanIfMatches(launchConfigLogsVisiblePattern, s, launchConfig::setLogsVisible); /* 优先. */
|
||||
setListIfMatchesWithKey(assetsPattern, s, "assets", projectConfig::setAssets, projectConfig, detectConflicts);
|
||||
setListIfMatchesWithKey(abisPattern, s, "abis", projectConfig::setAbis, projectConfig, detectConflicts);
|
||||
setListIfMatchesWithKey(libsPattern, s, "libs", projectConfig::setLibs, projectConfig, detectConflicts);
|
||||
setListIfMatchesWithKey(useFeaturesPattern, s, "useFeatures", projectConfig::setFeatures, projectConfig, detectConflicts);
|
||||
|
||||
setFieldForBooleanIfMatches(launchConfigDisplaySplashPattern, s, launchConfig::setSplashVisible);
|
||||
setFieldForBooleanIfMatches(launchConfigSplashVisiblePattern, s, launchConfig::setSplashVisible); /* 优先. */
|
||||
setListIfMatchesWithKey(permissionsPattern, s, "permissions", projectConfig::setPermissions, projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(signatureSchemePattern, s, "signatureScheme", projectConfig::setSignatureScheme, projectConfig, detectConflicts);
|
||||
|
||||
setListIfMatchesWithKey(excludeDirsPattern, s, "excludeDirs", excludeDirs -> excludeDirs.forEach(projectConfig::excludeDir), projectConfig, detectConflicts);
|
||||
setListIfMatchesWithKey(excludedDirsPattern, s, "excludeDirs", excludedDirs -> excludedDirs.forEach(projectConfig::excludeDir), projectConfig, detectConflicts);
|
||||
|
||||
setFieldForDoubleIfMatchesWithKey(buildTimePattern, s, "buildTime", buildTime -> buildInfo.setBuildTime((long) buildTime), projectConfig, detectConflicts);
|
||||
setFieldForDoubleIfMatchesWithKey(buildNumberPattern, s, "buildNumber", buildNumber -> buildInfo.setBuildNumber((long) buildNumber), projectConfig, detectConflicts);
|
||||
setFieldIfMatchesWithKey(buildIdPattern, s, "buildId", buildInfo::setBuildId, projectConfig, detectConflicts);
|
||||
|
||||
setFieldForBooleanIfMatchesWithKey(launchConfigHideLogsPattern, s, "hideLogs", value -> launchConfig.setLogsVisible(!value), projectConfig, detectConflicts);
|
||||
setFieldForBooleanIfMatchesWithKey(launchConfigLogsVisiblePattern, s, "logsVisible", launchConfig::setLogsVisible, projectConfig, detectConflicts); /* 优先. */
|
||||
|
||||
setFieldForBooleanIfMatchesWithKey(launchConfigDisplaySplashPattern, s, "displaySplash", launchConfig::setSplashVisible, projectConfig, detectConflicts);
|
||||
setFieldForBooleanIfMatchesWithKey(launchConfigSplashVisiblePattern, s, "splashVisible", launchConfig::setSplashVisible, projectConfig, detectConflicts); /* 优先. */
|
||||
|
||||
if (projectConfig.getName() == null || projectConfig.getName().isBlank()) {
|
||||
if (jsonFilePath.endsWith(CONFIG_FILE_NAME)) {
|
||||
@@ -301,26 +350,34 @@ public class ProjectConfig {
|
||||
|
||||
@NotNull
|
||||
@Language("RegExp")
|
||||
private static String listPattern(String name) {
|
||||
return "\"" + parseNamePattern(name) + "(?:s|list)?\"\\s*:\\s*\\[\\s*(\"(?:\\\\.|[^\"\\\\])*\")(?:\\s*,\\s*\"(?:\\\\.|[^\"\\\\])*\")*\\s*]";
|
||||
private static String listPatternWithKeyCapture(String name) {
|
||||
// Capture both the matched key name and the list content.
|
||||
// zh-CN: 同时捕获命中的 key 名称与数组内容.
|
||||
return "\"(" + parseNamePattern(name) + "(?:s|list)?)\"\\s*:\\s*\\[\\s*((?:\"(?:\\\\.|[^\"\\\\])*\"\\s*,\\s*)*\"(?:\\\\.|[^\"\\\\])*\")?\\s*]";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Language("RegExp")
|
||||
private static String numberPattern(String name) {
|
||||
return "\"" + parseNamePattern(name) + "\"\\s*:\\s*\"?(\\d+)\"?";
|
||||
private static String numberPatternWithKeyCapture(String name) {
|
||||
// Capture both the matched key name and the numeric content.
|
||||
// zh-CN: 同时捕获命中的 key 名称与数值内容.
|
||||
return "\"(" + parseNamePattern(name) + ")\"\\s*:\\s*\"?(\\d+)\"?";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Language("RegExp")
|
||||
private static String booleanPattern(String name) {
|
||||
return "\"" + parseNamePattern(name) + "\"\\s*:\\s*\"?(true|false)\"?";
|
||||
private static String booleanPatternWithKeyCapture(String name) {
|
||||
// Capture both the matched key name and the boolean content.
|
||||
// zh-CN: 同时捕获命中的 key 名称与布尔值内容.
|
||||
return "\"(" + parseNamePattern(name) + ")\"\\s*:\\s*\"?(true|false)\"?";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Language("RegExp")
|
||||
private static String stringPattern(String name) {
|
||||
return "\"" + parseNamePattern(name) + "\"\\s*:\\s*\"((?:[^\"]|(?<=\\\\)\")*?)(?<!\\\\)\"";
|
||||
private static String stringPatternWithKeyCapture(String name) {
|
||||
// Capture both the matched key name and the string content.
|
||||
// zh-CN: 同时捕获命中的 key 名称与字符串内容.
|
||||
return "\"(" + parseNamePattern(name) + ")\"\\s*:\\s*\"((?:[^\"]|(?<=\\\\)\")*?)(?<!\\\\)\"";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -328,42 +385,115 @@ public class ProjectConfig {
|
||||
return name.replaceAll("(?<=[a-z])([A-Z]+)", "(?:$1|_$1)");
|
||||
}
|
||||
|
||||
private static void setFieldIfMatches(Pattern pattern, String s, java.util.function.Consumer<String> setter) {
|
||||
private static void setFieldIfMatchesWithKey(
|
||||
Pattern pattern,
|
||||
String s,
|
||||
String canonicalKey,
|
||||
java.util.function.Consumer<String> setter,
|
||||
ProjectConfig recorder,
|
||||
boolean detectConflicts
|
||||
) {
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
if (matcher.find()) {
|
||||
setter.accept(matcher.group(1));
|
||||
String lastMatchedKey = null;
|
||||
String lastValue = null;
|
||||
int matches = 0;
|
||||
while (matcher.find()) {
|
||||
matches++;
|
||||
lastMatchedKey = matcher.group(1);
|
||||
lastValue = matcher.group(2);
|
||||
}
|
||||
if (matches == 0) return;
|
||||
if (detectConflicts && matches > 1) {
|
||||
throw new IllegalArgumentException("Conflicting keys for \"" + canonicalKey + "\" in fallback parsing");
|
||||
}
|
||||
recorder.recordOriginalJsonKey(canonicalKey, Objects.requireNonNull(lastMatchedKey));
|
||||
setter.accept(lastValue);
|
||||
}
|
||||
|
||||
private static void setFieldForDoubleIfMatches(Pattern pattern, String s, java.util.function.DoubleConsumer setter) {
|
||||
private static void setFieldForDoubleIfMatchesWithKey(
|
||||
Pattern pattern,
|
||||
String s,
|
||||
String canonicalKey,
|
||||
java.util.function.DoubleConsumer setter,
|
||||
ProjectConfig recorder,
|
||||
boolean detectConflicts
|
||||
) {
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
if (matcher.find()) {
|
||||
setter.accept(Double.parseDouble(Objects.requireNonNull(matcher.group(1))));
|
||||
String lastMatchedKey = null;
|
||||
String lastValue = null;
|
||||
int matches = 0;
|
||||
while (matcher.find()) {
|
||||
matches++;
|
||||
lastMatchedKey = matcher.group(1);
|
||||
lastValue = matcher.group(2);
|
||||
}
|
||||
if (matches == 0) return;
|
||||
if (detectConflicts && matches > 1) {
|
||||
throw new IllegalArgumentException("Conflicting keys for \"" + canonicalKey + "\" in fallback parsing");
|
||||
}
|
||||
recorder.recordOriginalJsonKey(canonicalKey, Objects.requireNonNull(lastMatchedKey));
|
||||
setter.accept(Double.parseDouble(Objects.requireNonNull(lastValue)));
|
||||
}
|
||||
|
||||
private static void setFieldForBooleanIfMatches(Pattern pattern, String s, java.util.function.Consumer<Boolean> setter) {
|
||||
private static void setFieldForBooleanIfMatchesWithKey(
|
||||
Pattern pattern,
|
||||
String s,
|
||||
String canonicalKey,
|
||||
java.util.function.Consumer<Boolean> setter,
|
||||
ProjectConfig recorder,
|
||||
boolean detectConflicts
|
||||
) {
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
if (matcher.find()) {
|
||||
setter.accept(Boolean.getBoolean(Objects.requireNonNull(matcher.group(1))));
|
||||
String lastMatchedKey = null;
|
||||
String lastValue = null;
|
||||
int matches = 0;
|
||||
while (matcher.find()) {
|
||||
matches++;
|
||||
lastMatchedKey = matcher.group(1);
|
||||
lastValue = matcher.group(2);
|
||||
}
|
||||
if (matches == 0) return;
|
||||
if (detectConflicts && matches > 1) {
|
||||
throw new IllegalArgumentException("Conflicting keys for \"" + canonicalKey + "\" in fallback parsing");
|
||||
}
|
||||
recorder.recordOriginalJsonKey(canonicalKey, Objects.requireNonNull(lastMatchedKey));
|
||||
setter.accept(Boolean.parseBoolean(Objects.requireNonNull(lastValue)));
|
||||
}
|
||||
|
||||
private static void setListIfMatches(Pattern pattern, String s, java.util.function.Consumer<List<String>> setter) {
|
||||
private static void setListIfMatchesWithKey(
|
||||
Pattern pattern,
|
||||
String s,
|
||||
String canonicalKey,
|
||||
java.util.function.Consumer<List<String>> setter,
|
||||
ProjectConfig recorder,
|
||||
boolean detectConflicts
|
||||
) {
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
if (matcher.find()) {
|
||||
String content = matcher.group(1);
|
||||
if (content == null) {
|
||||
setter.accept(Collections.emptyList());
|
||||
} else {
|
||||
List<String> list = Arrays.stream(content.split("\\s*,\\s*")).map(str -> {
|
||||
if (str.startsWith("\"") && str.endsWith("\"")) {
|
||||
return str.substring(1, str.length() - 1);
|
||||
}
|
||||
return str;
|
||||
}).collect(Collectors.toList());
|
||||
setter.accept(list);
|
||||
}
|
||||
String lastMatchedKey = null;
|
||||
String lastContent = null;
|
||||
int matches = 0;
|
||||
while (matcher.find()) {
|
||||
matches++;
|
||||
lastMatchedKey = matcher.group(1);
|
||||
lastContent = matcher.group(2);
|
||||
}
|
||||
if (matches == 0) return;
|
||||
if (detectConflicts && matches > 1) {
|
||||
throw new IllegalArgumentException("Conflicting keys for \"" + canonicalKey + "\" in fallback parsing");
|
||||
}
|
||||
|
||||
recorder.recordOriginalJsonKey(canonicalKey, Objects.requireNonNull(lastMatchedKey));
|
||||
|
||||
if (lastContent == null || lastContent.isBlank()) {
|
||||
setter.accept(Collections.emptyList());
|
||||
} else {
|
||||
List<String> list = Arrays.stream(lastContent.split("\\s*,\\s*")).map(str -> {
|
||||
if (str.startsWith("\"") && str.endsWith("\"")) {
|
||||
return str.substring(1, str.length() - 1);
|
||||
}
|
||||
return str;
|
||||
}).collect(Collectors.toList());
|
||||
setter.accept(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,8 +640,56 @@ public class ProjectConfig {
|
||||
mLaunchConfig = launchConfig;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return sGson.toJson(this);
|
||||
@Override
|
||||
public void recordOriginalJsonKey(@NonNull String canonicalKey, @NonNull String originalKey) {
|
||||
// Always overwrite to keep the "last key wins" behavior.
|
||||
// zh-CN: 始终覆盖, 以保持 "最后一个 key 生效" 的行为.
|
||||
mOriginalJsonKeys.put(canonicalKey, originalKey);
|
||||
}
|
||||
|
||||
public String toJson(boolean detectConflicts) {
|
||||
// Serialize first using canonical keys, then rename keys to preserve the original ones.
|
||||
// zh-CN: 先按 canonical key 序列化, 再重命名 key 以保留原始 key.
|
||||
JsonElement tree = sGson.toJsonTree(this);
|
||||
if (!tree.isJsonObject()) {
|
||||
return sGson.toJson(this);
|
||||
}
|
||||
|
||||
JsonObject obj = tree.getAsJsonObject();
|
||||
|
||||
// Rename canonical keys to original keys if needed.
|
||||
// zh-CN: 如有需要, 将 canonical key 重命名为原始 key.
|
||||
for (Map.Entry<String, String> entry : mOriginalJsonKeys.entrySet()) {
|
||||
String canonicalKey = entry.getKey();
|
||||
String originalKey = entry.getValue();
|
||||
|
||||
if (canonicalKey == null || originalKey == null) {
|
||||
continue;
|
||||
}
|
||||
if (canonicalKey.equals(originalKey)) {
|
||||
continue;
|
||||
}
|
||||
if (!obj.has(canonicalKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.has(originalKey)) {
|
||||
// Detect or resolve conflicts depending on the flag.
|
||||
// zh-CN: 根据开关选择检测或静默解决冲突.
|
||||
if (detectConflicts) {
|
||||
throw new IllegalStateException("Conflicting keys when serializing: \"" + canonicalKey + "\" and \"" + originalKey + "\"");
|
||||
}
|
||||
// Keep the existing original key and drop canonical key silently.
|
||||
// zh-CN: 静默保留原始 key, 丢弃 canonical key.
|
||||
obj.remove(canonicalKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonElement value = obj.remove(canonicalKey);
|
||||
obj.add(originalKey, value);
|
||||
}
|
||||
|
||||
return sGson.toJson(obj);
|
||||
}
|
||||
|
||||
public String getIconPath() {
|
||||
@@ -618,7 +796,7 @@ public class ProjectConfig {
|
||||
}
|
||||
|
||||
public List<String> getPermissions() {
|
||||
return mPermissions;
|
||||
return Objects.requireNonNullElse(mPermissions, DEFAULT_PERMISSIONS);
|
||||
}
|
||||
|
||||
public ProjectConfig setPermissions(List<String> permissions) {
|
||||
|
||||
@@ -87,7 +87,7 @@ import static org.autojs.autojs.util.StringUtils.key;
|
||||
|
||||
/**
|
||||
* Created by Stardust on Oct 22, 2017.
|
||||
* Modified by SuperMonster003 as of Dec 1, 2023.
|
||||
* Modified by SuperMonster003 as of Jan 6, 2026.
|
||||
*
|
||||
* @noinspection ResultOfMethodCallIgnored
|
||||
*/
|
||||
@@ -553,30 +553,33 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
checkBox.setTextSize(12);
|
||||
int marginInPixels = (int) (8 * getResources().getDisplayMetrics().density);
|
||||
checkBox.setPadding(marginInPixels, 0, 0, 0);
|
||||
if (mProjectConfig != null) {
|
||||
boolean checked = mProjectConfig.getPermissions().stream().anyMatch(p -> {
|
||||
var lc = p.toLowerCase(Locale.ROOT);
|
||||
var uc = p.toUpperCase(Locale.ROOT);
|
||||
if (p.equalsIgnoreCase(permission)) {
|
||||
return true;
|
||||
}
|
||||
if (permission.contains("android")) {
|
||||
String refined = uc.substring(uc.lastIndexOf(".") + 1).replaceAll("\\W", "_");
|
||||
return Objects.equals(refined, permission.substring(permission.lastIndexOf(".") + 1));
|
||||
}
|
||||
if (PERMISSION_ALIAS.containsKey(lc)) {
|
||||
return Objects.equals(permission, PERMISSION_ALIAS.get(lc));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
checkBox.setChecked(checked);
|
||||
} else {
|
||||
checkBox.setChecked(false);
|
||||
}
|
||||
List<String> permissions = mProjectConfig != null
|
||||
? mProjectConfig.getPermissions()
|
||||
: ProjectConfig.DEFAULT_PERMISSIONS;
|
||||
boolean checked = hasPermission(permissions, permission);
|
||||
checkBox.setChecked(checked);
|
||||
mFlexboxPermissionsView.addView(checkBox);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasPermission(List<String> permissions, String permission) {
|
||||
return permissions.stream().anyMatch(p -> {
|
||||
var lc = p.toLowerCase(Locale.ROOT);
|
||||
var uc = p.toUpperCase(Locale.ROOT);
|
||||
if (p.equalsIgnoreCase(permission)) {
|
||||
return true;
|
||||
}
|
||||
if (permission.contains("android")) {
|
||||
String refined = uc.substring(uc.lastIndexOf(".") + 1).replaceAll("\\W", "_");
|
||||
return Objects.equals(refined, permission.substring(permission.lastIndexOf(".") + 1));
|
||||
}
|
||||
if (PERMISSION_ALIAS.containsKey(lc)) {
|
||||
return Objects.equals(permission, PERMISSION_ALIAS.get(lc));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isAliasMatching(Map<String, List<String>> aliases, String aliasKey, List<String> candidates) {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
var aliasList = aliases.getOrDefault(aliasKey, Collections.emptyList());
|
||||
|
||||
@@ -22,11 +22,11 @@ import org.autojs.autojs.model.explorer.ExplorerDirPage
|
||||
import org.autojs.autojs.model.explorer.ExplorerFileItem
|
||||
import org.autojs.autojs.model.explorer.Explorers
|
||||
import org.autojs.autojs.model.project.ProjectTemplate
|
||||
import org.autojs.autojs.pio.PFiles.ensureDir
|
||||
import org.autojs.autojs.pio.PFiles.write
|
||||
import org.autojs.autojs.pio.PFiles
|
||||
import org.autojs.autojs.project.ProjectConfig
|
||||
import org.autojs.autojs.theme.ThemeColorHelper
|
||||
import org.autojs.autojs.ui.BaseActivity
|
||||
import org.autojs.autojs.ui.error.ErrorDialogActivity
|
||||
import org.autojs.autojs.ui.shortcut.AppsIconSelectActivity
|
||||
import org.autojs.autojs.ui.widget.SimpleTextWatcher
|
||||
import org.autojs.autojs.util.ViewUtils
|
||||
@@ -184,13 +184,13 @@ class ProjectConfigActivity : BaseActivity() {
|
||||
finish()
|
||||
}) { e: Throwable ->
|
||||
e.printStackTrace()
|
||||
ViewUtils.showToast(this, e.message, true)
|
||||
ErrorDialogActivity.showErrorDialog(this, R.string.error_failed_to_save_project_config, e.message)
|
||||
}
|
||||
} else {
|
||||
Observable.fromCallable {
|
||||
write(
|
||||
PFiles.write(
|
||||
ProjectConfig.configFileOfDir(mDirectory!!.path),
|
||||
mProjectConfig!!.toJson()
|
||||
mProjectConfig!!.toJson(true)
|
||||
)
|
||||
Void.TYPE
|
||||
}
|
||||
@@ -202,7 +202,7 @@ class ProjectConfigActivity : BaseActivity() {
|
||||
finish()
|
||||
}) { e: Throwable ->
|
||||
e.printStackTrace()
|
||||
ViewUtils.showToast(this, e.message, true)
|
||||
ErrorDialogActivity.showErrorDialog(this, R.string.error_failed_to_save_project_config, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,7 @@ class ProjectConfigActivity : BaseActivity() {
|
||||
iconPath = "res/logo.png"
|
||||
}
|
||||
val iconFile = File(mDirectory, iconPath)
|
||||
ensureDir(iconFile.path)
|
||||
PFiles.ensureDir(iconFile.path)
|
||||
val fos = FileOutputStream(iconFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.close()
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
package org.autojs.autojs.util
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.autojs.autojs.annotation.DeserializedMethodName
|
||||
import org.autojs.autojs.annotation.SerializedNameCompatible
|
||||
import org.json.JSONTokener
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Created by SuperMonster003 on Nov 20, 2024.
|
||||
@@ -114,91 +104,4 @@ object JsonUtils {
|
||||
@JvmStatic
|
||||
fun isValidJson(json: String) = json.isNotBlank() && runCatching { JSONTokener(json).nextValue() }.isSuccess
|
||||
|
||||
class FuzzyDeserializer<T> : JsonDeserializer<T> {
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T {
|
||||
require(typeOfT is Class<*>) {
|
||||
"Expected parameter typeOfT to be of type Class, but got: ${typeOfT?.javaClass?.name}"
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val clazz = typeOfT as Class<T>
|
||||
val instance = try {
|
||||
clazz.getDeclaredConstructor().newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Failed to create an instance of ${clazz.name}", e)
|
||||
}
|
||||
if (json is JsonObject) {
|
||||
clazz.declaredFields.forEach { processField(it, json, instance, context) }
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
private fun processField(field: Field, json: JsonObject, instance: T, context: JsonDeserializationContext?) {
|
||||
field.isAccessible = true
|
||||
|
||||
val serializedNameAnnotation = field.getAnnotation(SerializedName::class.java)
|
||||
val compatibleAnnotation = field.getAnnotation(SerializedNameCompatible::class.java)
|
||||
val deserializedAnnotation = field.getAnnotation(DeserializedMethodName::class.java)
|
||||
|
||||
val sanitizedPrimaryName = sanitizeKey(serializedNameAnnotation?.value ?: field.name)
|
||||
val sanitizedAlternateNames = serializedNameAnnotation?.alternate?.map { sanitizeKey(it) } ?: emptyList()
|
||||
val sanitizedCompatibleNames = compatibleAnnotation?.with?.map { sanitizeKey(it.value) to it.isReversed } ?: emptyList()
|
||||
|
||||
val serializedNames: List<Pair<String, Boolean>> = (sanitizedAlternateNames + sanitizedPrimaryName).map { it to false } + sanitizedCompatibleNames
|
||||
|
||||
json.entrySet().forEach { (jsonKey, jsonValue) ->
|
||||
val sanitizedJsonKey = sanitizeKey(jsonKey)
|
||||
serializedNames.forEach { pair ->
|
||||
val (serializedKey, isReversed) = pair
|
||||
if (sanitizedJsonKey == serializedKey) {
|
||||
when {
|
||||
deserializedAnnotation != null -> {
|
||||
handleDeserializedMethod(field, instance, jsonValue, deserializedAnnotation)
|
||||
}
|
||||
field.type == Boolean::class.javaPrimitiveType || field.type == Boolean::class.java -> {
|
||||
if (jsonValue.isJsonPrimitive && jsonValue.asJsonPrimitive.isBoolean) {
|
||||
val value = jsonValue.asBoolean
|
||||
field.set(instance, if (isReversed) !value else value)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!jsonValue.isJsonNull) {
|
||||
val value = context?.deserialize<Any>(jsonValue, field.genericType)
|
||||
field.set(instance, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return@processField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeserializedMethod(field: Field, instance: T, jsonValue: JsonElement, annotation: DeserializedMethodName) {
|
||||
require(jsonValue.isJsonPrimitive && jsonValue.asJsonPrimitive.isString) {
|
||||
"Field with @DeserializedMethodName must map to a JSON string."
|
||||
}
|
||||
val methodName = annotation.method
|
||||
val methodInput = jsonValue.asString
|
||||
val parameterTypes = annotation.parameterTypes.map { it.java }.toTypedArray()
|
||||
val method = runCatching {
|
||||
instance!!::class.java.getMethod(methodName, *parameterTypes)
|
||||
}.getOrElse { e ->
|
||||
throw RuntimeException("Method $methodName with parameter types ${parameterTypes.contentToString()} not found in ${instance!!::class.java.name}", e)
|
||||
}
|
||||
try {
|
||||
val isStatic = Modifier.isStatic(method.modifiers)
|
||||
val result = method.invoke(instance.takeUnless { isStatic }, methodInput)
|
||||
field.set(instance, result)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Failed to invoke method $methodName on ${field.name}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeKey(key: String): String {
|
||||
return key.replace(Regex("[^a-zA-Z0-9]"), "").lowercase()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1136,8 +1136,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">جارٍ جلب سجل التغييرات...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">جارٍ جلب ملاحظات الإصدار...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">المحرّك</string>
|
||||
<string name="text_scheduled_restart_start_delay">تأخير البدء</string>
|
||||
<string name="dialog_button_advanced_settings">متقدم</string>
|
||||
<string name="error_failed_to_save_project_config">تعذّر حفظ إعدادات المشروع</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1134,5 +1134,6 @@
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1134,8 +1134,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">Obteniendo el registro de cambios...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">Obteniendo las notas de la versión...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">Motor</string>
|
||||
<string name="text_scheduled_restart_start_delay">Retraso de inicio</string>
|
||||
<string name="dialog_button_advanced_settings">Avanzado</string>
|
||||
<string name="error_failed_to_save_project_config">No se pudo guardar la configuración del proyecto</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1134,8 +1134,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">Récupération du journal des modifications...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">Récupération des notes de version...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">Moteur</string>
|
||||
<string name="text_scheduled_restart_start_delay">Délai de démarrage</string>
|
||||
<string name="dialog_button_advanced_settings">Avancé</string>
|
||||
<string name="error_failed_to_save_project_config">Échec de l\'enregistrement de la configuration du projet</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1135,8 +1135,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">更新ログを取得しています...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">リリースノートを取得しています...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">エンジン</string>
|
||||
<string name="text_scheduled_restart_start_delay">開始遅延</string>
|
||||
<string name="dialog_button_advanced_settings">詳細</string>
|
||||
<string name="error_failed_to_save_project_config">プロジェクト設定の保存に失敗しました</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1136,8 +1136,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">변경 로그를 가져오는 중...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">릴리스 노트를 가져오는 중...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">엔진</string>
|
||||
<string name="text_scheduled_restart_start_delay">시작 지연</string>
|
||||
<string name="dialog_button_advanced_settings">고급</string>
|
||||
<string name="error_failed_to_save_project_config">프로젝트 설정을 저장하지 못했습니다</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1134,8 +1134,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">Получение списка изменений...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">Получение примечаний к выпуску...</string>
|
||||
<string name="dialog_button_view_with_browser">@string/dialog_button_download_with_browser</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">Движок</string>
|
||||
<string name="text_scheduled_restart_start_delay">Задержка старта</string>
|
||||
<string name="dialog_button_advanced_settings">Доп.</string>
|
||||
<string name="error_failed_to_save_project_config">Не удалось сохранить конфигурацию проекта</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1132,8 +1132,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">正在獲取更新日誌...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">正在獲取發行説明...</string>
|
||||
<string name="dialog_button_view_with_browser">瀏覽器查看</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">調度引擎</string>
|
||||
<string name="text_scheduled_restart_start_delay">啓動延遲</string>
|
||||
<string name="dialog_button_advanced_settings">高級設置</string>
|
||||
<string name="error_failed_to_save_project_config">保存項目配置失敗</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1132,8 +1132,9 @@
|
||||
<string name="text_retrieving_changelog" tools:ignore="TypographyEllipsis">正在獲取更新日誌...</string>
|
||||
<string name="text_retrieving_release_notes" tools:ignore="TypographyEllipsis">正在獲取發行說明...</string>
|
||||
<string name="dialog_button_view_with_browser">瀏覽器檢視</string>
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="text_scheduled_restart_backend">排程引擎</string>
|
||||
<string name="text_scheduled_restart_start_delay">啟動延遲</string>
|
||||
<string name="dialog_button_advanced_settings">高階設定</string>
|
||||
<string name="error_failed_to_save_project_config">儲存專案配置失敗</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1135,5 +1135,6 @@
|
||||
<string name="text_scheduled_restart_backend">调度引擎</string>
|
||||
<string name="text_scheduled_restart_start_delay">启动延迟</string>
|
||||
<string name="dialog_button_advanced_settings">高级设置</string>
|
||||
<string name="error_failed_to_save_project_config">保存项目配置失败</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1392,5 +1392,6 @@
|
||||
<string name="text_scheduled_restart_backend">Backend</string>
|
||||
<string name="text_scheduled_restart_start_delay">Start delay</string>
|
||||
<string name="dialog_button_advanced_settings">Advanced</string>
|
||||
<string name="error_failed_to_save_project_config">Failed to save project config</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#Sat Jan 03 20:14:36 CST 2026
|
||||
BUILD_TIME=1767442476618
|
||||
#Tue Jan 06 21:47:58 CST 2026
|
||||
BUILD_TIME=1767707278134
|
||||
COMPILE_SDK_VERSION=36
|
||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
|
||||
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||
TARGET_SDK_VERSION=36
|
||||
TARGET_SDK_VERSION_INRT=29
|
||||
VERSION_BUILD=3576
|
||||
VERSION_BUILD=3581
|
||||
VERSION_NAME=6.7.0 Alpha13
|
||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
||||
|
||||
Reference in New Issue
Block a user