From 7e59981147396b354c9ff27ffe0fe3e1e0bd32cb Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Wed, 5 Feb 2025 21:07:38 +0800 Subject: [PATCH] =?UTF-8?q?6.6.2=20-=20Alpha2=20-=20project.json=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20signatureScheme=20=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=AE=BD=E6=9D=BE=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .readme/template_readme.md | 2 +- .../annotation/DeserializedMethodName.java | 14 ++ .../autojs/autojs/apkbuilder/ApkBuilder.kt | 179 +++++++++++------- .../autojs/autojs/project/ProjectConfig.java | 114 +++++++---- .../autojs/ui/project/BuildActivity.java | 114 ++++++----- .../java/org/autojs/autojs/util/JsonUtils.kt | 70 ++++++- version.properties | 6 +- 7 files changed, 326 insertions(+), 173 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/annotation/DeserializedMethodName.java diff --git a/.readme/template_readme.md b/.readme/template_readme.md index 5e4c9296..023ae4c6 100644 --- a/.readme/template_readme.md +++ b/.readme/template_readme.md @@ -55,7 +55,7 @@ | [Auto.js Pro 9](https://pro.autojs.org/) | AutoJsPro | [hyb1996](https://github.com/hyb1996) | `2021/03/28` | `2023/02/09` | | [AutoJs6](https://github.com/SuperMonster003/AutoJs6) | [AutoJs6](https://github.com/SuperMonster003/AutoJs6/commit/a8ce1b9acb541e9736c33134be3194c3148a15a3#diff-833a46a97033e77558372a2dce103fd6fee29aaaa899f610022a7aece592ee7bR27) | [SuperMonster003](https://github.com/SuperMonster003) | `2021/12/01` | - | | [AutoX](https://github.com/kkevsekk1/AutoX) | [Autox.js v6](https://github.com/kkevsekk1/AutoX/commit/8b6776cff8b0fca4be4a52719b7d7d07c0a058f3#diff-51a0b488f963eb0be6c6599bf5df497313877cf5bdff3950807373912ac1cdc9R130) | [wilinz](https://github.com/wilinz) | `2022/05/26` | - | -| [AutoX](https://github.com/kkevsekk1/AutoX) | [Autox.js v7](https://github.com/kkevsekk1/AutoX/commit/484491fd5fe12b8203d0b09c181eb0f471c0ea9f#diff-8cff73265af19c059547b76aca8882cbaa3209291406f52df1dafbbc78e80c46R120) | [aiselp](https://github.com/aiselp) | `2024/04/21` | - | +| [AutoX](https://github.com/aiselp/AutoX) | [Autox.js v7](https://github.com/aiselp/AutoX/commit/484491fd5fe12b8203d0b09c181eb0f471c0ea9f#diff-8cff73265af19c059547b76aca8882cbaa3209291406f52df1dafbbc78e80c46R120) | [aiselp](https://github.com/aiselp) | `2024/04/21` | - | {{ p_introduction_related_autojs_table_date_is_estimated }}. diff --git a/app/src/main/java/org/autojs/autojs/annotation/DeserializedMethodName.java b/app/src/main/java/org/autojs/autojs/annotation/DeserializedMethodName.java new file mode 100644 index 00000000..3ef56752 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/annotation/DeserializedMethodName.java @@ -0,0 +1,14 @@ +package org.autojs.autojs.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DeserializedMethodName { + String method(); + + Class[] parameterTypes() default {}; +} 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 06ba38d1..374692ec 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt @@ -16,8 +16,9 @@ import org.autojs.autojs.app.GlobalAppContext import org.autojs.autojs.engine.encryption.AdvancedEncryptionStandard import org.autojs.autojs.pio.PFiles import org.autojs.autojs.project.BuildInfo +import org.autojs.autojs.project.LaunchConfig import org.autojs.autojs.project.ProjectConfig -import org.autojs.autojs.project.ProjectConfig.DEFAULT_MAIN_SCRIPT_FILE_NAME +import org.autojs.autojs.project.ProjectConfig.CONFIG_FILE_NAME import org.autojs.autojs.script.EncryptedScriptFileHeader.writeHeader import org.autojs.autojs.script.JavaScriptFileSource import org.autojs.autojs.util.FileUtils.TYPE.JAVASCRIPT @@ -39,7 +40,7 @@ import java.io.InputStream * Created by Stardust on Oct 24, 2017. * Modified by SuperMonster003 as of Jul 8, 2022. */ -open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File, private val workspacePath: String) { +open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File, private val buildPath: String) { private var mProgressCallback: ProgressCallback? = null private var mArscPackageName: String? = null @@ -49,7 +50,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File private lateinit var mProjectConfig: ProjectConfig - private val mApkPackager = ApkPackager(apkInputStream, workspacePath) + private val mApkPackager = ApkPackager(apkInputStream, buildPath) private val mAssetManager: AssetManager by lazy { globalContext.assets } @@ -57,14 +58,14 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File private var mAssetsFileIncludes = Libs.defaultAssetFilesToInclude.toMutableList() private var mAssetsDirExcludes = Libs.defaultAssetDirsToExclude.toMutableList() - private var mSplashThemeId = 0 - private var mNoSplashThemeId = 0 + private var mSplashThemeId: Int = 0 + private var mNoSplashThemeId: Int = 0 private val mManifestFile - get() = File(workspacePath, "AndroidManifest.xml") + get() = File(buildPath, "AndroidManifest.xml") private val mResourcesArscFile - get() = File(workspacePath, "resources.arsc") + get() = File(buildPath, "resources.arsc") init { PFiles.ensureDir(outApkFile.path) @@ -75,7 +76,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File @Throws(IOException::class) fun prepare() = also { mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onPrepare(this) } } - File(workspacePath).mkdirs() + File(buildPath).mkdirs() mApkPackager.unzip() } @@ -97,7 +98,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File @Throws(IOException::class) fun copyDir(srcFile: File, relativeDestPath: String) { - val destDirFile = File(workspacePath, relativeDestPath).apply { mkdir() } + val destDirFile = File(buildPath, relativeDestPath).apply { mkdir() } srcFile.listFiles()?.forEach { srcChildFile -> if (srcChildFile.isFile) { if (srcChildFile.name.endsWith(JAVASCRIPT.extensionWithDot)) { @@ -133,7 +134,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File @Throws(IOException::class) fun replaceFile(srcFile: File, relativeDestPath: String) = also { - val destFile = File(workspacePath, relativeDestPath) + val destFile = File(buildPath, relativeDestPath) if (destFile.name.endsWith(JAVASCRIPT.extensionWithDot)) { encrypt(srcFile, destFile) } else { @@ -144,70 +145,99 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File @Throws(IOException::class) fun withConfig(config: ProjectConfig) = also { config.also { mProjectConfig = it }.run { - if (!launchConfig.isSplashVisible) { - try { - val tableBlock = TableBlock.load(mResourcesArscFile) - val packageName = "${GlobalAppContext.get().packageName}.inrt" - val packageBlock = tableBlock.getOrCreatePackage(0x7f, packageName).also { - tableBlock.currentPackage = it - } - packageBlock.getEntry("", "style", "AppTheme.Splash")?.let { - mSplashThemeId = it.resourceId - } - packageBlock.getEntry("", "style", "AppTheme.SevereTransparent")?.let { - mNoSplashThemeId = it.resourceId - } - } catch (e: Exception) { - e.printStackTrace() - } - } - mManifestEditor = editManifest() - .setAppName(name) - .setVersionName(versionName) - .setVersionCode(versionCode) - .setPackageName(packageName) - + retrieveSplashThemeResources(launchConfig) + prepareManifestConfiguration(this) setArscPackageName(packageName) updateProjectConfig(this) - copyAssetsRecursively("", File(workspacePath, "assets")) + copyAssetsRecursively("", File(buildDir, "assets")) copyLibrariesByConfig(this) setScriptFile(sourcePath) } } + private fun prepareManifestConfiguration(config: ProjectConfig) { + mManifestEditor = editManifest() + .setAppName(config.name) + .setVersionName(config.versionName) + .setVersionCode(config.versionCode) + .setPackageName(config.packageName) + } + + private fun retrieveSplashThemeResources(launchConfig: LaunchConfig) { + if (launchConfig.isSplashVisible) { + // @Hint by SuperMonster003 on Jan 23, 2024. + // ! Members `mSplashThemeId` and `mNoSplashThemeId` will keep their default values. + // ! zh-CN: 成员变量 `mSplashThemeId` 及 `mNoSplashThemeId` 将保持其默认值. + return + } + try { + val tableBlock = TableBlock.load(mResourcesArscFile) + val packageName = "${GlobalAppContext.get().packageName}.inrt" + val packageBlock = tableBlock.getOrCreatePackage(0x7f, packageName).also { + tableBlock.currentPackage = it + } + packageBlock.getEntry("", "style", "AppTheme.Splash")?.let { + mSplashThemeId = it.resourceId + } + packageBlock.getEntry("", "style", "AppTheme.SevereTransparent")?.let { + mNoSplashThemeId = it.resourceId + } + } catch (e: Exception) { + e.printStackTrace() + } + } + @Throws(FileNotFoundException::class) fun editManifest(): ManifestEditor = ManifestEditorWithAuthorities(FileInputStream(mManifestFile)).also { mManifestEditor = it } private fun updateProjectConfig(config: ProjectConfig) { - val projectConfig = when { - !PFiles.isDir(config.sourcePath) -> null - else -> ProjectConfig.fromProjectDir(config.sourcePath)?.also { - val buildNumber = it.buildInfo.buildNumber - it.buildInfo = BuildInfo.generate(buildNumber + 1) - PFiles.write(ProjectConfig.configFileOfDir(config.sourcePath), it.toJson()) + + // 这里为什么要有这样的一个方法? ( + // 会不会是因为有些配置需要写入到文件中, 这些配置包括自增的版本号, 用户的选择或键入值等等 + // ) + // 参数 config 只是获取了一部分的字段用于设置新的 projectConfig, 为什么不直接使用全部的字段? ( + // 因为有些不需要更新. 如果我们需要将所有设置开放到 Activity 页面中, 那么其实所有配置都是需要更新的 + // ) + // 像 abis, libs, signatureScheme 等信息就全部丢失了. ( + // 所以需要添加到这个方法中 + // ) + + val projectConfig = run { + if (PFiles.isDir(config.sourcePath)) { + // @Hint by SuperMonster003 on Jan 23, 2025. + // ! Project directory packaging. + // ! zh-CN: 打包项目目录. + ProjectConfig.fromProjectDir(config.sourcePath)?.let { sourceProjectConfig -> + sourceProjectConfig + .setBuildInfo(BuildInfo.generate(sourceProjectConfig.buildInfo.buildNumber + 1)) + File(ProjectConfig.configFileOfDir(config.sourcePath)).writeText(sourceProjectConfig.toJson()) + return@run sourceProjectConfig + } } - } ?: ProjectConfig() - .setMainScriptFileName(DEFAULT_MAIN_SCRIPT_FILE_NAME) - .setName(config.name) - .setPackageName(config.packageName) - .setVersionName(config.versionName) - .setVersionCode(config.versionCode) - .also { newProjectConfig -> - newProjectConfig.buildInfo = BuildInfo.generate(newProjectConfig.versionCode.toLong()) - File(workspacePath, "assets/project/${ProjectConfig.CONFIG_FILE_NAME}").also { file -> + // @Hint by SuperMonster003 on Jan 23, 2025. + // ! Single file packaging. + // ! zh-CN: 打包单独文件. + return@run ProjectConfig().also { newProjectConfig -> + newProjectConfig + .setName(config.name) + .setPackageName(config.packageName) + .setVersionName(config.versionName) + .setVersionCode(config.versionCode) + .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()) } - projectConfig.run { - mKey = MD5Utils.md5(packageName + versionName + mainScriptFileName) - mInitVector = MD5Utils.md5(buildInfo.buildId + name).substring(0, 16) - Libs.entries.forEach { entry -> - if (config.libs.contains(entry.label)) { - mLibsIncludes += entry.libsToInclude.toSet() - mAssetsFileIncludes += entry.assetFilesToInclude.toSet() - mAssetsDirExcludes -= entry.assetDirsToExclude.toSet() - } + } + + mKey = MD5Utils.md5(projectConfig.run { packageName + versionName + mainScriptFileName }) + mInitVector = MD5Utils.md5(projectConfig.run { buildInfo.buildId + name }).take(16) + Libs.entries.forEach { entry -> + if (config.libs.contains(entry.label)) { + mLibsIncludes += entry.libsToInclude.toSet() + mAssetsFileIncludes += entry.assetFilesToInclude.toSet() + mAssetsDirExcludes -= entry.assetDirsToExclude.toSet() } } } @@ -225,7 +255,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File val appIcon = packageBlock.getOrCreate("", ICON_RES_DIR, ICON_NAME) val appIconPath = appIcon.resValue.decodeValue() Log.d(TAG, "Icon path: $appIconPath") - val file = File(workspacePath, appIconPath).also { + val file = File(buildPath, appIconPath).also { if (!it.exists()) { File(it.parent!!).mkdirs() it.createNewFile() @@ -273,19 +303,20 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File fun sign() = also { mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onSign(this) } } val fos = FileOutputStream(outApkFile) - TinySign.sign(File(workspacePath), fos) + TinySign.sign(File(buildPath), fos) fos.close() - val defaultKeyStoreFile = File(workspacePath, "default_key_store.bks") - val tmpOutputApk = File(workspacePath, "temp.apk") + val defaultKeyStoreFile = File(buildPath, "default_key_store.bks") + val tmpOutputApk = File(buildPath, "temp.apk") copyInputStreamToFile(GlobalAppContext.get().assets.open("default_key_store.bks"), defaultKeyStoreFile) - val signer = ApkSigner(outApkFile, tmpOutputApk) - signer.useDefaultSignatureVersion = false - signer.v1SigningEnabled = mProjectConfig.signatureScheme.contains("V1") - signer.v2SigningEnabled = mProjectConfig.signatureScheme.contains("V2") - signer.v3SigningEnabled = mProjectConfig.signatureScheme.contains("V3") - signer.v4SigningEnabled = mProjectConfig.signatureScheme.contains("V4") + val signer = ApkSigner(outApkFile, tmpOutputApk).apply { + useDefaultSignatureVersion = false + v1SigningEnabled = "V1" in mProjectConfig.signatureScheme + v2SigningEnabled = "V2" in mProjectConfig.signatureScheme + v3SigningEnabled = "V3" in mProjectConfig.signatureScheme + v4SigningEnabled = "V4" in mProjectConfig.signatureScheme + } var keyStoreFile = defaultKeyStoreFile var password = "AutoJs6" @@ -313,7 +344,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File fun cleanWorkspace() = also { mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onClean(this) } } - delete(File(workspacePath)) + delete(File(buildPath)) } @Throws(IOException::class) @@ -321,8 +352,8 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File @Throws(IOException::class) private fun buildArsc() { - val oldArsc = File(workspacePath, "resources.arsc") - val newArsc = File(workspacePath, "resources.arsc.new") + val oldArsc = File(buildPath, "resources.arsc") + val newArsc = File(buildPath, "resources.arsc.new") val decoder = ARSCDecoder(BufferedInputStream(FileInputStream(oldArsc)), null, false) decoder.CloneArsc(FileOutputStream(newArsc), mArscPackageName, true) oldArsc.delete() @@ -345,9 +376,13 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File private inner class ManifestEditorWithAuthorities(manifestInputStream: InputStream?) : ManifestEditor(manifestInputStream) { override fun onAttr(attr: AxmlWriter.Attr) { attr.apply { + + // @Reference to aiselp (https://github.com/aiselp) by SuperMonster003 on Jan 18, 2025. + // ! https://github.com/aiselp/AutoX/blob/5b3303926082d591a166b1845702357406811aaf/app/src/main/java/org/autojs/autojs/build/ApkBuilder.kt#L175-L188 if (!mProjectConfig.launchConfig.isSplashVisible && mSplashThemeId != 0 && value == mSplashThemeId) { value = mNoSplashThemeId } + if (name.data == "authorities" && value is StringItem) { (value as StringItem).data = "${mProjectConfig.packageName}.fileprovider" } else { @@ -388,7 +423,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File mLibsIncludes.distinct().forEach { libName -> runCatching { File(srcLibDir, "$abiSrcName/$libName").takeIf { it.exists() }?.copyTo( - File(workspacePath, "lib/$abiDestName/$libName"), + File(buildPath, "lib/$abiDestName/$libName"), overwrite = true ) }.onFailure { it.printStackTrace() } 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 d6cf7189..507e21bc 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,7 @@ import androidx.annotation.Nullable; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; +import org.autojs.autojs.annotation.DeserializedMethodName; import org.autojs.autojs.annotation.SerializedNameCompatible; import org.autojs.autojs.annotation.SerializedNameCompatible.With; import org.autojs.autojs.apkbuilder.keystore.KeyStore; @@ -22,14 +23,13 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.Set; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Created by Stardust on Jan 24, 2018. @@ -49,18 +49,24 @@ public class ProjectConfig { .create(); @SerializedName("name") - @SerializedNameCompatible(with = {@With(value = "projectName")}) + @SerializedNameCompatible(with = { + @With(value = "projectName"), + }) private String mName; @SerializedName("versionName") - @SerializedNameCompatible(with = {@With(value = "version")}) + @SerializedNameCompatible(with = { + @With(value = "version"), + }) private String mVersionName; @SerializedName("versionCode") - private int mVersionCode = -1; + private int mVersionCode = 1; @SerializedName("packageName") - @SerializedNameCompatible(with = {@With(value = "package")}) + @SerializedNameCompatible(with = { + @With(value = "package"), + }) private String mPackageName; @SerializedName("main") @@ -73,7 +79,7 @@ public class ProjectConfig { @With(value = "mainFile"), @With(value = "mainFileName"), }) - private String mMainScriptFileName; + private String mMainScriptFileName = DEFAULT_MAIN_SCRIPT_FILE_NAME; @Nullable @SerializedName(value = "assets") @@ -84,15 +90,21 @@ public class ProjectConfig { private List mAssets = new ArrayList<>(); @SerializedName("launchConfig") - @SerializedNameCompatible(with = {@With(value = "launch")}) + @SerializedNameCompatible(with = { + @With(value = "launch"), + }) private LaunchConfig mLaunchConfig = new LaunchConfig(); @SerializedName("build") - @SerializedNameCompatible(with = {@With(value = "buildInfo")}) + @SerializedNameCompatible(with = { + @With(value = "buildInfo"), + }) private BuildInfo mBuildInfo = new BuildInfo(); @SerializedName("icon") - @SerializedNameCompatible(with = {@With(value = "iconPath")}) + @SerializedNameCompatible(with = { + @With(value = "iconPath"), + }) private String mIconPath; private transient Callable mIconBitmapGetter; @@ -125,19 +137,23 @@ public class ProjectConfig { @With(value = "signatureSchemes"), @With(value = "signature"), }) + @DeserializedMethodName(method = "normalizeSignatureScheme", parameterTypes = {String.class}) private String mSignatureScheme = "V1 + V2"; @Nullable private transient KeyStore mKeyStore = null; - @SerializedName("scriptConfigs") - @SerializedNameCompatible(with = { - @With(value = "scriptsConfigs"), - @With(value = "scriptsConfig"), - @With(value = "scriptConfig"), - @With(value = "scripts", target = {"AutoJs4", "AutoX"}), - }) - private final Map mScriptConfigs = new HashMap<>(); + // @Commented by SuperMonster003 on Jan 20, 2025. + // ! Unused config options: "scripts". + // ! zh-CN: 未使用的配置选项: "scripts". + // # @SerializedName("scriptConfigs") + // # @SerializedNameCompatible(with = { + // # @With(value = "scriptsConfigs"), + // # @With(value = "scriptsConfig"), + // # @With(value = "scriptConfig"), + // # @With(value = "scripts", target = {"AutoJs4", "AutoX"}), + // # }) + // # private final Map mScriptConfigs = new HashMap<>(); @SerializedName(value = "useFeatures") @SerializedNameCompatible(with = { @@ -362,8 +378,9 @@ public class ProjectConfig { return mBuildInfo; } - public void setBuildInfo(BuildInfo buildInfo) { + public ProjectConfig setBuildInfo(BuildInfo buildInfo) { mBuildInfo = buildInfo; + return this; } public String getName() { @@ -412,7 +429,7 @@ public class ProjectConfig { @NonNull public String getMainScriptFileName() { - return mMainScriptFileName != null ? mMainScriptFileName : DEFAULT_MAIN_SCRIPT_FILE_NAME; + return mMainScriptFileName; } public ProjectConfig setMainScriptFileName(String mainScriptFileName) { @@ -420,9 +437,12 @@ public class ProjectConfig { return this; } - public Map getScriptConfigs() { - return mScriptConfigs; - } + // @Commented by SuperMonster003 on Jan 20, 2025. + // ! Unused config options: "scripts". + // ! zh-CN: 未使用的配置选项: "scripts". + // # public Map getScriptConfigs() { + // # return mScriptConfigs; + // # } public List getAssets() { if (mAssets == null) { @@ -516,22 +536,20 @@ public class ProjectConfig { mFeatures = features; } - public ScriptConfig getScriptConfig(String path) { - ScriptConfig scriptConfig = Objects.requireNonNull(mScriptConfigs.getOrDefault(path, new ScriptConfig())); - List combinedFeatures = getCombinedFeatures(scriptConfig); - scriptConfig.setFeatures(combinedFeatures); - return scriptConfig; - } - - @NotNull - public ArrayList getCombinedFeatures(ScriptConfig scriptConfig) { - return new ArrayList<>( - new HashSet<>() {{ - addAll(scriptConfig.getFeatures()); - addAll(mFeatures); - }} - ); - } + // @Commented by SuperMonster003 on Jan 20, 2025. + // ! Unused config options: "scripts". + // ! zh-CN: 未使用的配置选项: "scripts". + // # public ScriptConfig getScriptConfig(String scriptKeyName) { + // # ScriptConfig scriptConfig = Objects.requireNonNull(mScriptConfigs.getOrDefault(scriptKeyName, new ScriptConfig())); + // # List combinedFeatures = new ArrayList<>( + // # new HashSet<>() {{ + // # addAll(scriptConfig.getFeatures()); + // # addAll(mFeatures); + // # }} + // # ); + // # scriptConfig.setFeatures(combinedFeatures); + // # return scriptConfig; + // # } public List getExcludedDirs() { return mExcludedDirs; @@ -566,10 +584,24 @@ public class ProjectConfig { } public ProjectConfig setSignatureScheme(String signatureScheme) { - mSignatureScheme = signatureScheme; + mSignatureScheme = normalizeSignatureScheme(signatureScheme); return this; } + public static String normalizeSignatureScheme(String input) { + Pattern pattern = Pattern.compile("v\\d+", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(input); + Set matches = new HashSet<>(); + while (matcher.find()) { + matches.add(matcher.group().toUpperCase()); + } + if (matches.isEmpty()) { + return input.trim(); + } + return matches.stream().sorted().collect(Collectors.joining(" + ")); + } + + @Nullable public KeyStore getKeyStore() { return mKeyStore; 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 aad29a68..f61a54b4 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 @@ -21,6 +21,7 @@ import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import androidx.lifecycle.ViewModelProvider; import com.afollestad.materialdialogs.MaterialDialog; import com.google.android.flexbox.FlexboxLayout; @@ -70,6 +71,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.autojs.autojs.apkbuilder.ApkBuilder.TEMPLATE_APK_NAME; import static org.autojs.autojs.util.StringUtils.key; @@ -77,6 +79,8 @@ import static org.autojs.autojs.util.StringUtils.key; /** * Created by Stardust on Oct 22, 2017. * Modified by SuperMonster003 as of Dec 1, 2023. + * + * @noinspection ResultOfMethodCallIgnored */ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCallback { @@ -105,14 +109,14 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa private static final Map> LIB_ALIASES = new HashMap<>(); - private static final ArrayList SIGNATURE_SCHEMES = new ArrayList<>() {{ - add("V1 + V2"); - add("V1 + V3"); - add("V1 + V2 + V3"); - add("V1"); - add("V2 + V3 (Android 7.0+)"); - add("V2 (Android 7.0+)"); - add("V3 (Android 9.0+)"); + private static final List> SIGNATURE_SCHEMES = new ArrayList<>() {{ + add(new Pair<>("V1 + V2", null)); + add(new Pair<>("V1 + V3", null)); + add(new Pair<>("V1 + V2 + V3", null)); + add(new Pair<>("V1", null)); + add(new Pair<>("V2 + V3", "Android 7.0+")); + add(new Pair<>("V2", "Android 7.0+")); + add(new Pair<>("V3", "Android 9.0+")); }}; private final Map SUPPORTED_PERMISSIONS = new TreeMap<>() {{ @@ -173,16 +177,19 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa ImageView mIconView; LinearLayout mAppConfigView; + @Nullable private ProjectConfig mProjectConfig; + private MaterialDialog mProgressDialog; private String mSource; private boolean mIsDefaultIcon = true; private boolean mIsProjectLevelBuilding; - private FlexboxLayout mFlexboxAbis; - private FlexboxLayout mFlexboxLibs; - private Spinner mSignatureSchemes; - private Spinner mVerifiedKeyStores; - private FlexboxLayout mFlexboxPermissions; + + private FlexboxLayout mFlexboxAbisView; + private FlexboxLayout mFlexboxLibsView; + private Spinner mSignatureSchemesView; + private Spinner mVerifiedKeyStoresView; + private FlexboxLayout mFlexboxPermissionsView; private final ArrayList mInvalidAbis = new ArrayList<>(); private final ArrayList mUnavailableAbis = new ArrayList<>(); @@ -261,35 +268,30 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa mAppConfigView = binding.appConfig; - mFlexboxAbis = binding.flexboxAbis; + mFlexboxAbisView = binding.flexboxAbis; initAbisChildren(); - mFlexboxLibs = binding.flexboxLibraries; + mFlexboxLibsView = binding.flexboxLibraries; initLibsChildren(); mKeyStoreViewModel = new ViewModelProvider(this, new KeyStoreViewModel.Factory(getApplicationContext())).get(KeyStoreViewModel.class); mKeyStoreViewModel.updateVerifiedKeyStores(); - mSignatureSchemes = binding.spinnerSignatureSchemes; - initSignatureSchemeSpinner(); - - mVerifiedKeyStores = binding.spinnerVerifiedKeyStores; - initVerifiedKeyStoresSpinner(); - - mFlexboxPermissions = binding.flexboxPermissions; - initPermissionsChildren(); + mSignatureSchemesView = binding.spinnerSignatureSchemes; + mVerifiedKeyStoresView = binding.spinnerVerifiedKeyStores; + mFlexboxPermissionsView = binding.flexboxPermissions; binding.fab.setOnClickListener(v -> buildApk()); binding.selectSource.setOnClickListener(v -> selectSourceFilePath()); binding.selectOutput.setOnClickListener(v -> selectOutputDirPath()); - binding.textAbis.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxAbis)); + binding.textAbis.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxAbisView)); binding.textAbis.setOnLongClickListener(v -> { syncAbisCheckedStates(); return true; }); - binding.textLibs.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxLibs)); + binding.textLibs.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxLibsView)); binding.manageKeyStore.setOnClickListener(v -> ManageKeyStoreActivity.Companion.startActivity(this)); - binding.textPermissions.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxPermissions)); + binding.textPermissions.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxPermissionsView)); setToolbarAsBack(R.string.text_build_apk); mSource = getIntent().getStringExtra(EXTRA_SOURCE); @@ -297,6 +299,10 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa setupWithSourceFile(new ScriptFile(mSource)); } + initSignatureSchemeSpinner(); + initVerifiedKeyStoresSpinner(); + initPermissionsChildren(); + syncAbisCheckedStates(); syncLibsCheckedStates(); @@ -354,7 +360,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa child.setChecked(false); child.setEnabled(false); child.setOnBeingUnavailableListener(this::promptForUnavailability); - mFlexboxAbis.addView(child); + mFlexboxAbisView.addView(child); }); } @@ -397,8 +403,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa private void syncAbisWithDefaultCheckedFilter(Function filterForDefaultChecked) { List appSupportedAbiList = AndroidUtils.getAppSupportedAbiList(); - for (int i = 0; i < mFlexboxAbis.getChildCount(); i += 1) { - View child = mFlexboxAbis.getChildAt(i); + for (int i = 0; i < mFlexboxAbisView.getChildCount(); i += 1) { + View child = mFlexboxAbisView.getChildAt(i); if (child instanceof RoundCheckboxWithText) { CharSequence standardAbi = ((RoundCheckboxWithText) child).getText(); if (standardAbi != null) { @@ -419,7 +425,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa RoundCheckboxWithText child = new RoundCheckboxWithText(this, null); child.setText(text); child.setChecked(false); - mFlexboxLibs.addView(child); + mFlexboxLibsView.addView(child); }); } @@ -432,8 +438,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa // 创建一个新的副本 var candidates = new ArrayList<>(configLibs); - for (int i = 0; i < mFlexboxLibs.getChildCount(); i += 1) { - View child = mFlexboxLibs.getChildAt(i); + for (int i = 0; i < mFlexboxLibsView.getChildCount(); i += 1) { + View child = mFlexboxLibsView.getChildAt(i); if (child instanceof RoundCheckboxWithText) { CharSequence standardLib = ((RoundCheckboxWithText) child).getText(); if (standardLib != null) { @@ -447,9 +453,23 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } private void initSignatureSchemeSpinner() { - ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, SIGNATURE_SCHEMES); + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, SIGNATURE_SCHEMES.stream().map(pair -> { + if (pair.second == null || pair.second.isEmpty()) { + return pair.first; + } + return pair.first + " (" + pair.second + ")"; + }).collect(Collectors.toList())); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mSignatureSchemes.setAdapter(adapter); + mSignatureSchemesView.setAdapter(adapter); + if (mProjectConfig != null) { + int initialSelection = IntStream.range(0, SIGNATURE_SCHEMES.size()) + .filter(i -> SIGNATURE_SCHEMES.get(i).first.equalsIgnoreCase(mProjectConfig.getSignatureScheme())) + .findFirst() + .orElse(-1); + if (initialSelection >= 0) { + mSignatureSchemesView.setSelection(initialSelection); + } + } } private void initVerifiedKeyStoresSpinner() { @@ -460,7 +480,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, verifiedKeyStores); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mVerifiedKeyStores.setAdapter(adapter); + mVerifiedKeyStoresView.setAdapter(adapter); mKeyStoreViewModel.getVerifiedKeyStores().observe(this, keyStores -> { // 清空现有的选项,但保留第一个元素,即默认密钥库 @@ -487,7 +507,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa int marginInPixels = (int) (8 * getResources().getDisplayMetrics().density); checkBox.setPadding(marginInPixels, 0, 0, 0); checkBox.setChecked(false); - mFlexboxPermissions.addView(checkBox); + mFlexboxPermissionsView.addView(checkBox); }); } @@ -644,8 +664,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } private boolean checkAbis() { - for (int i = 0; i < mFlexboxAbis.getChildCount(); i += 1) { - View child = mFlexboxAbis.getChildAt(i); + for (int i = 0; i < mFlexboxAbisView.getChildCount(); i += 1) { + View child = mFlexboxAbisView.getChildAt(i); if (child instanceof RoundCheckboxWithText) { if (((RoundCheckboxWithText) child).isChecked() && child.isEnabled()) { return true; @@ -774,11 +794,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa @SuppressLint("CheckResult") private void doBuildingApk() { ProjectConfig projectConfig = determineProjectConfig(); - File tmpDir = new File(getCacheDir(), "build/"); + File buildPath = new File(getCacheDir(), "build/"); File outApk = new File(mOutputPathView.getText().toString(), String.format("%s_v%s.apk", projectConfig.getName(), projectConfig.getVersionName())); showProgressDialog(); - Observable.fromCallable(() -> callApkBuilder(tmpDir, outApk, projectConfig)) + Observable.fromCallable(() -> callApkBuilder(buildPath, outApk, projectConfig)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(apkBuilder -> { @@ -791,9 +811,9 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } private ProjectConfig determineProjectConfig() { - ArrayList abis = collectCheckedItems(mFlexboxAbis); - ArrayList libs = collectCheckedItems(mFlexboxLibs); - ArrayList permissions = collectCheckedItems(mFlexboxPermissions); + ArrayList abis = collectCheckedItems(mFlexboxAbisView); + ArrayList libs = collectCheckedItems(mFlexboxLibsView); + ArrayList permissions = collectCheckedItems(mFlexboxPermissionsView); ProjectConfig projectConfig; if (mProjectConfig != null) { @@ -814,8 +834,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa return projectConfig .setAbis(abis) .setLibs(libs) - .setKeyStore(mVerifiedKeyStores.getSelectedItemPosition() > 0 ? (KeyStore) mVerifiedKeyStores.getSelectedItem() : null) - .setSignatureScheme(mSignatureSchemes.getSelectedItem().toString()) + .setKeyStore(mVerifiedKeyStoresView.getSelectedItemPosition() > 0 ? (KeyStore) mVerifiedKeyStoresView.getSelectedItem() : null) + .setSignatureScheme(mSignatureSchemesView.getSelectedItem().toString()) .setPermissions(permissions); } @@ -844,9 +864,9 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa return libs; } - private ApkBuilder callApkBuilder(File tmpDir, File outApk, ProjectConfig projectConfig) throws Exception { + private ApkBuilder callApkBuilder(File buildPath, File outApk, ProjectConfig projectConfig) throws Exception { InputStream templateApk = getAssets().open(TEMPLATE_APK_NAME); - return new ApkBuilder(templateApk, outApk, tmpDir.getPath()) + return new ApkBuilder(templateApk, outApk, buildPath.getPath()) .setProgressCallback(BuildActivity.this) .prepare() .withConfig(projectConfig) 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 57193021..95b6d14c 100644 --- a/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/JsonUtils.kt @@ -5,10 +5,11 @@ 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.JSONArray -import org.json.JSONObject +import org.json.JSONTokener import java.lang.reflect.Field +import java.lang.reflect.Modifier import java.lang.reflect.Type /** @@ -84,7 +85,34 @@ object JsonUtils { return json } - fun isValidJson(json: String): Boolean = runCatching { JSONObject(json) }.isSuccess || runCatching { JSONArray(json) }.isSuccess + /** + * This method checks whether the given string is a valid JSON format. + * + * This method supports the following common JSON formats: + * - Objects (e.g., `{"key": "value"}`) + * - Arrays (e.g., `[1, 2, 3]`) + * - Single values (e.g., `"string"`, `123`, `true`, `null`) + * + * Note: + * 1. Empty strings or strings containing only whitespace are considered invalid JSON. + * 2. If the JSON contains comments (e.g., `// comment` or `/* multi-line comment */`), + * this method will consider it invalid. Because comments are not allowed in strict JSON standards. + * + * zh-CN: + * + * 判断给定的字符串是否是有效的 JSON 格式. + * + * 支持以下几种常见 JSON 格式: + * - 对象 (例如: `{"key": "value"}`) + * - 数组 (例如: `[1, 2, 3]`) + * - 单一值 (例如: `"string"`, `123`, `true`, `null`) + * + * 注意: + * 1. 空字符串或仅包含空白字符的字符串被视为无效的 JSON. + * 2. 如果 JSON 中包含注释 (例如: `// 注释` 或 `/* 多行注释 */`), 此方法会判断为无效. 因为注释不符合严格的 JSON 规范. + */ + @JvmStatic + fun isValidJson(json: String) = json.isNotBlank() && runCatching { JSONTokener(json).nextValue() }.isSuccess class FuzzyDeserializer : JsonDeserializer { @@ -99,7 +127,6 @@ object JsonUtils { } 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) } } @@ -111,6 +138,7 @@ object JsonUtils { 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() @@ -118,13 +146,16 @@ object JsonUtils { val serializedNames: List> = (sanitizedAlternateNames + sanitizedPrimaryName).map { it to false } + sanitizedCompatibleNames - for ((jsonKey, jsonValue) in json.entrySet()) { + json.entrySet().forEach { (jsonKey, jsonValue) -> val sanitizedJsonKey = sanitizeKey(jsonKey) - for (pair in serializedNames) { + serializedNames.forEach { pair -> val (serializedKey, isReversed) = pair if (sanitizedJsonKey == serializedKey) { - when (field.type) { - Boolean::class.javaPrimitiveType, Boolean::class.java -> { + 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) @@ -137,12 +168,33 @@ object JsonUtils { } } } - return + 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() } diff --git a/version.properties b/version.properties index b6feca74..8067396f 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Mon Jan 20 15:15:12 CST 2025 -BUILD_TIME=1737357312345 +#Wed Feb 05 20:51:07 CST 2025 +BUILD_TIME=1738759867266 COMPILE_SDK_VERSION=34 JAVA_VERSION=23 JAVA_VERSION_MIN_RADICAL=0 @@ -17,6 +17,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=34 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=2979 +VERSION_BUILD=2982 VERSION_NAME=6.6.2 Alpha2 VSCODE_EXT_REQUIRED_VERSION=1.0.8