6.6.2 - Alpha2 - project.json 支持 signatureScheme 选项宽松匹配

This commit is contained in:
SuperMonster003
2025-02-05 21:07:38 +08:00
parent 08c64bf248
commit 7e59981147
7 changed files with 326 additions and 173 deletions

View File

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

View File

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

View File

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

View File

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

View File

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