From 76862c50fe05fd866fa56fd26f72ba4a2b602503 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Tue, 6 Jan 2026 22:26:35 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha13=20-=20=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E9=A1=B5=E9=9D=A2=E9=BB=98=E8=AE=A4=E5=8B=BE?= =?UTF-8?q?=E9=80=89=E5=BF=85=E8=A6=81=E6=9D=83=E9=99=90=20(issue=20#397);?= =?UTF-8?q?=20=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E9=94=AE=E5=90=8D?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BF=AE=E6=94=B9=E4=BF=9D=E6=8A=A4=E5=B9=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 9 +- .../autojs/autojs/apkbuilder/ApkBuilder.kt | 4 +- .../autojs/model/project/ProjectTemplate.java | 2 +- .../autojs/project/FuzzyDeserializer.kt | 161 +++++++++ .../autojs/autojs/project/ProjectConfig.java | 332 ++++++++++++++---- .../autojs/ui/project/BuildActivity.java | 45 +-- .../ui/project/ProjectConfigActivity.kt | 14 +- .../java/org/autojs/autojs/util/JsonUtils.kt | 97 ----- app/src/main/res/values-ar/strings.xml | 7 +- app/src/main/res/values-en/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 7 +- app/src/main/res/values-fr/strings.xml | 7 +- app/src/main/res/values-ja/strings.xml | 7 +- app/src/main/res/values-ko/strings.xml | 7 +- app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values-zh-rHK/strings.xml | 7 +- app/src/main/res/values-zh-rTW/strings.xml | 7 +- app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + version.properties | 6 +- 20 files changed, 494 insertions(+), 235 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 0d815f3b..0f0b5325 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -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 以降低构建环境差异" ], diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt index c52a86a9..d487f822 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt @@ -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)) } } diff --git a/app/src/main/java/org/autojs/autojs/model/project/ProjectTemplate.java b/app/src/main/java/org/autojs/autojs/model/project/ProjectTemplate.java index fd71bfdc..aedaec79 100644 --- a/app/src/main/java/org/autojs/autojs/model/project/ProjectTemplate.java +++ b/app/src/main/java/org/autojs/autojs/model/project/ProjectTemplate.java @@ -25,7 +25,7 @@ public class ProjectTemplate { public Observable 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; }) diff --git a/app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt b/app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt new file mode 100644 index 00000000..3b712ef7 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/project/FuzzyDeserializer.kt @@ -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 @JvmOverloads constructor( + private val detectConflicts: Boolean = false, +) : JsonDeserializer { + + 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 + 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> = + (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(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) + + } + +} diff --git a/app/src/main/java/org/autojs/autojs/project/ProjectConfig.java b/app/src/main/java/org/autojs/autojs/project/ProjectConfig.java index a2cd68e6..1b3d2885 100644 --- a/app/src/main/java/org/autojs/autojs/project/ProjectConfig.java +++ b/app/src/main/java/org/autojs/autojs/project/ProjectConfig.java @@ -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 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()) - .registerTypeAdapter(LaunchConfig.class, new JsonUtils.FuzzyDeserializer()) - .registerTypeAdapter(ScriptConfig.class, new JsonUtils.FuzzyDeserializer()) - .registerTypeAdapter(BuildInfo.class, new JsonUtils.FuzzyDeserializer()) + .registerTypeAdapter(ProjectConfig.class, new FuzzyDeserializer()) + .registerTypeAdapter(LaunchConfig.class, new FuzzyDeserializer()) + .registerTypeAdapter(ScriptConfig.class, new FuzzyDeserializer()) + .registerTypeAdapter(BuildInfo.class, new FuzzyDeserializer()) .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 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 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 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 mIconBitmapGetter; + @Nullable @SerializedName(value = "abis") @SerializedNameCompatible(with = { @@ -118,6 +148,7 @@ public class ProjectConfig { @With(value = "abiList"), }) private List mAbis = new ArrayList<>(); + @Nullable @SerializedName(value = "libs") @SerializedNameCompatible(with = { @@ -125,12 +156,14 @@ public class ProjectConfig { @With(value = "libList"), }) private List mLibs = new ArrayList<>(); + @SerializedName("permissions") @SerializedNameCompatible(with = { @With(value = "permission"), @With(value = "permissionList"), }) - private List mPermissions = new ArrayList<>(); + private List 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 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 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*\"((?:[^\"]|(?<=\\\\)\")*?)(? setter) { + private static void setFieldIfMatchesWithKey( + Pattern pattern, + String s, + String canonicalKey, + java.util.function.Consumer 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 setter) { + private static void setFieldForBooleanIfMatchesWithKey( + Pattern pattern, + String s, + String canonicalKey, + java.util.function.Consumer 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> setter) { + private static void setListIfMatchesWithKey( + Pattern pattern, + String s, + String canonicalKey, + java.util.function.Consumer> 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 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 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 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 getPermissions() { - return mPermissions; + return Objects.requireNonNullElse(mPermissions, DEFAULT_PERMISSIONS); } public ProjectConfig setPermissions(List permissions) { diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java index 97c2d914..27f63c66 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java @@ -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 permissions = mProjectConfig != null + ? mProjectConfig.getPermissions() + : ProjectConfig.DEFAULT_PERMISSIONS; + boolean checked = hasPermission(permissions, permission); + checkBox.setChecked(checked); mFlexboxPermissionsView.addView(checkBox); }); } + private boolean hasPermission(List 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> aliases, String aliasKey, List candidates) { AtomicBoolean result = new AtomicBoolean(false); var aliasList = aliases.getOrDefault(aliasKey, Collections.emptyList()); diff --git a/app/src/main/java/org/autojs/autojs/ui/project/ProjectConfigActivity.kt b/app/src/main/java/org/autojs/autojs/ui/project/ProjectConfigActivity.kt index 5236d40d..0c4d2bdc 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/ProjectConfigActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/project/ProjectConfigActivity.kt @@ -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() diff --git a/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt b/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt index 95b6d14c..35e840a1 100644 --- a/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt @@ -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 : JsonDeserializer { - - 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 - 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> = (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(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() - } - - } - } \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4d12c027..4af6c1b9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1136,8 +1136,9 @@ جارٍ جلب سجل التغييرات... جارٍ جلب ملاحظات الإصدار... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + المحرّك + تأخير البدء + متقدم + تعذّر حفظ إعدادات المشروع diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index b5226812..896640b6 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1134,5 +1134,6 @@ Backend Start delay Advanced + Failed to save project config diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0bc8bafa..a9cf2a8e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1134,8 +1134,9 @@ Obteniendo el registro de cambios... Obteniendo las notas de la versión... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + Motor + Retraso de inicio + Avanzado + No se pudo guardar la configuración del proyecto diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5add513b..8d13f011 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1134,8 +1134,9 @@ Récupération du journal des modifications... Récupération des notes de version... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + Moteur + Délai de démarrage + Avancé + Échec de l\'enregistrement de la configuration du projet diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 979b8bd6..fb5c43e6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1135,8 +1135,9 @@ 更新ログを取得しています... リリースノートを取得しています... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + エンジン + 開始遅延 + 詳細 + プロジェクト設定の保存に失敗しました diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 10dfc2e8..4ef08e7c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1136,8 +1136,9 @@ 변경 로그를 가져오는 중... 릴리스 노트를 가져오는 중... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + 엔진 + 시작 지연 + 고급 + 프로젝트 설정을 저장하지 못했습니다 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 39b209fd..ac241806 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1134,8 +1134,9 @@ Получение списка изменений... Получение примечаний к выпуску... @string/dialog_button_download_with_browser - Backend - Start delay - Advanced + Движок + Задержка старта + Доп. + Не удалось сохранить конфигурацию проекта diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 955f5150..edd09403 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1132,8 +1132,9 @@ 正在獲取更新日誌... 正在獲取發行説明... 瀏覽器查看 - Backend - Start delay - Advanced + 調度引擎 + 啓動延遲 + 高級設置 + 保存項目配置失敗 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1e7ef161..1cc4602f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1132,8 +1132,9 @@ 正在獲取更新日誌... 正在獲取發行說明... 瀏覽器檢視 - Backend - Start delay - Advanced + 排程引擎 + 啟動延遲 + 高階設定 + 儲存專案配置失敗 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index c64c735b..250f143c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1135,5 +1135,6 @@ 调度引擎 启动延迟 高级设置 + 保存项目配置失败 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e6d4819..cceddd88 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1392,5 +1392,6 @@ Backend Start delay Advanced + Failed to save project config diff --git a/version.properties b/version.properties index 1cdb66d1..c9e09ca7 100644 --- a/version.properties +++ b/version.properties @@ -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