6.7.0 - Alpha13 - 打包应用页面默认勾选必要权限 (issue #397); 配置文件键名增加修改保护并支持冲突检测

This commit is contained in:
SuperMonster003
2026-01-06 22:26:35 +08:00
parent c3b7d6359c
commit 76862c50fe
20 changed files with 494 additions and 235 deletions

View File

@@ -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 以降低构建环境差异"
],

View File

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

View File

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

View 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)
}
}

View File

@@ -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) {

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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