6.6.2 - Alpha2 - project.json 支持 signatureScheme 选项宽松匹配
This commit is contained in:
@@ -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 {};
|
||||
}
|
||||
@@ -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() }
|
||||
|
||||
@@ -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<String> 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<Bitmap> 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<String, ScriptConfig> 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<String, ScriptConfig> 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<String, ScriptConfig> getScriptConfigs() {
|
||||
return mScriptConfigs;
|
||||
}
|
||||
// @Commented by SuperMonster003 on Jan 20, 2025.
|
||||
// ! Unused config options: "scripts".
|
||||
// ! zh-CN: 未使用的配置选项: "scripts".
|
||||
// # public Map<String, ScriptConfig> getScriptConfigs() {
|
||||
// # return mScriptConfigs;
|
||||
// # }
|
||||
|
||||
public List<String> 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<String> combinedFeatures = getCombinedFeatures(scriptConfig);
|
||||
scriptConfig.setFeatures(combinedFeatures);
|
||||
return scriptConfig;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ArrayList<String> 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<String> combinedFeatures = new ArrayList<>(
|
||||
// # new HashSet<>() {{
|
||||
// # addAll(scriptConfig.getFeatures());
|
||||
// # addAll(mFeatures);
|
||||
// # }}
|
||||
// # );
|
||||
// # scriptConfig.setFeatures(combinedFeatures);
|
||||
// # return scriptConfig;
|
||||
// # }
|
||||
|
||||
public List<File> 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<String> 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;
|
||||
|
||||
@@ -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<String, List<String>> LIB_ALIASES = new HashMap<>();
|
||||
|
||||
private static final ArrayList<String> 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<Pair<String, String>> 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<String, Integer> 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<String> mInvalidAbis = new ArrayList<>();
|
||||
private final ArrayList<String> 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<String, Boolean> filterForDefaultChecked) {
|
||||
List<String> 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<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, SIGNATURE_SCHEMES);
|
||||
ArrayAdapter<String> 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<KeyStore> 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<String> abis = collectCheckedItems(mFlexboxAbis);
|
||||
ArrayList<String> libs = collectCheckedItems(mFlexboxLibs);
|
||||
ArrayList<String> permissions = collectCheckedItems(mFlexboxPermissions);
|
||||
ArrayList<String> abis = collectCheckedItems(mFlexboxAbisView);
|
||||
ArrayList<String> libs = collectCheckedItems(mFlexboxLibsView);
|
||||
ArrayList<String> 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)
|
||||
|
||||
@@ -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<T> : JsonDeserializer<T> {
|
||||
|
||||
@@ -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<Pair<String, Boolean>> = (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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user