From 3e9724d7ab5e87e4bce8846ee54a5ce6ba5660fb Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sat, 7 Mar 2026 13:01:14 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha23=20-=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=89=93=E5=8C=85=E8=BF=87=E7=A8=8B=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=96=B9=E5=BC=8F;=20=E6=89=93=E5=8C=85?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E6=94=AF=E6=8C=81=E6=93=8D=E4=BD=9C=E4=B8=AD?= =?UTF-8?q?=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../autojs/autojs/apkbuilder/ApkBuilder.kt | 623 ++++++++++++++++-- .../autojs/autojs/apkbuilder/ApkPackager.java | 58 +- .../autojs/apkbuilder/util/StreamUtils.java | 6 +- .../autojs/ui/project/BuildActivity.java | 448 ++++++++++++- app/src/main/res/drawable/ic_check_mark.png | Bin 0 -> 7261 bytes app/src/main/res/drawable/ic_right_arrow.png | Bin 0 -> 11042 bytes .../main/res/layout/dialog_build_progress.xml | 186 ++++++ app/src/main/res/values-ar/strings.xml | 68 +- app/src/main/res/values-en/strings.xml | 68 +- app/src/main/res/values-es/strings.xml | 66 +- app/src/main/res/values-fr/strings.xml | 66 +- app/src/main/res/values-ja/strings.xml | 66 +- app/src/main/res/values-ko/strings.xml | 68 +- app/src/main/res/values-ru/strings.xml | 66 +- app/src/main/res/values-zh-rHK/strings.xml | 68 +- app/src/main/res/values-zh-rTW/strings.xml | 68 +- app/src/main/res/values-zh/strings.xml | 68 +- app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 68 +- version.properties | 8 +- 20 files changed, 1875 insertions(+), 198 deletions(-) create mode 100644 app/src/main/res/drawable/ic_check_mark.png create mode 100644 app/src/main/res/drawable/ic_right_arrow.png create mode 100644 app/src/main/res/layout/dialog_build_progress.xml 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 f585deca..f55ecde8 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkBuilder.kt @@ -36,14 +36,18 @@ import pxb.android.StringItem import pxb.android.axml.AxmlWriter import zhao.arsceditor.ResDecoder.ARSCDecoder import java.io.BufferedInputStream +import java.io.BufferedOutputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -59,6 +63,9 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File private var mManifestEditor: ManifestEditor? = null private var mInitVector: String? = null private var mKey: String? = null + private var mCancelSignal: AtomicBoolean? = null + private var mPendingProjectConfigFile: File? = null + private var mPendingProjectConfigJson: String? = null private lateinit var mProjectConfig: ProjectConfig @@ -85,49 +92,157 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File fun setProgressCallback(callback: ProgressCallback?) = also { mProgressCallback = callback } - @Throws(IOException::class) - fun prepare() = also { - mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onPrepare(this) } } - File(buildPath).mkdirs() - mApkPackager.unzip() + fun setCancelSignal(cancelSignal: AtomicBoolean?) = also { + mCancelSignal = cancelSignal + mApkPackager.setCancelSignal(cancelSignal) + } + + // Throw early when cancellation is requested to avoid partial outputs. + // zh-CN: 当收到取消请求时尽早抛出, 以避免产生部分输出. + private fun ensureNotCancelled() { + if (mCancelSignal?.get() == true) { + throw CancellationException("Build aborted") + } + if (Thread.currentThread().isInterrupted) { + throw CancellationException("Build aborted") + } + } + + // Copy streams with cancellation checks to keep abort responsive during large IO. + // zh-CN: 在大 IO 过程中加入取消检查, 保持中止响应. + private fun copyStreamWithCancel(input: InputStream, output: OutputStream, bufferSize: Int = 16 * 1024) { + val buffer = ByteArray(bufferSize) + var len: Int + while (input.read(buffer).also { len = it } > 0) { + ensureNotCancelled() + output.write(buffer, 0, len) + } + } + + private fun notifyStepChanged(step: ProgressStep) { + mProgressCallback?.let { callback -> + GlobalAppContext.post { + when (step) { + ProgressStep.PREPARE -> callback.onPrepare(this) + ProgressStep.BUILD -> callback.onBuild(this) + ProgressStep.SIGN -> callback.onSign(this) + ProgressStep.CLEAN -> callback.onClean(this) + } + } + } + } + + private fun notifyStepProgress(step: ProgressStep, title: String, detail: String?) { + mProgressCallback?.let { callback -> + GlobalAppContext.post { + callback.onStepProgress( + builder = this, + title = title, + detail = detail?.takeIf { it.isNotBlank() }, + ) + } + } } @Throws(IOException::class) - fun setScriptFile(path: String?) = also { + fun prepare(context: Context) = also { + ensureNotCancelled() + notifyStepChanged(ProgressStep.PREPARE) + notifyStepProgress( + ProgressStep.PREPARE, + context.getString(R.string.text_preparing_workspace), + buildPath, + ) + File(buildPath).mkdirs() + notifyStepProgress( + ProgressStep.PREPARE, + context.getString(R.string.text_extracting_template_apk), + buildPath, + ) + mApkPackager.unzip() + ensureNotCancelled() + notifyStepProgress( + ProgressStep.PREPARE, + context.getString(R.string.text_prepare_completed), + buildPath, + ) + } + + @Throws(IOException::class) + fun setScriptFile(context: Context, path: String?) = also { + ensureNotCancelled() path?.let { when { - PFiles.isDir(it) -> copyDir(it, "assets/project/") - else -> replaceFile(it, "assets/project/main.js") + PFiles.isDir(it) -> { + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_project_directory), + it, + ) + copyDir(context, it, "assets/project/") + } + else -> { + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_script_file), + it, + ) + replaceFile(context, it, "assets/project/main.js") + } } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_source_processing_completed), + it, + ) } } @Throws(IOException::class) @Suppress("SameParameterValue") - private fun copyDir(srcPath: String, relativeDestPath: String) { - copyDir(File(srcPath), relativeDestPath) + private fun copyDir(context: Context, srcPath: String, relativeDestPath: String) { + copyDir(context, File(srcPath), relativeDestPath) } @Throws(IOException::class) - fun copyDir(srcFile: File, relativeDestPath: String) { + fun copyDir(context: Context, srcFile: File, relativeDestPath: String) { + ensureNotCancelled() val destDirFile = File(buildPath, relativeDestPath).apply { mkdir() } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_directory), + "${srcFile.path} -> ${destDirFile.path}", + ) srcFile.listFiles()?.forEach { srcChildFile -> + ensureNotCancelled() if (srcChildFile.isFile) { if (srcChildFile.name.endsWith(JAVASCRIPT.extensionWithDot)) { - encryptToDir(srcChildFile, destDirFile) + encryptToDir(context, srcChildFile, destDirFile) } else { - srcChildFile.copyTo(File(destDirFile, srcChildFile.name), true) + val destFile = File(destDirFile, srcChildFile.name) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_file), + "${srcChildFile.path} -> ${destFile.path}", + ) + srcChildFile.copyTo(destFile, true) } } else { if (!mProjectConfig.excludedDirs.contains(srcChildFile)) { - copyDir(srcChildFile, PFiles.join(relativeDestPath, srcChildFile.name + File.separator)) + copyDir(context, srcChildFile, PFiles.join(relativeDestPath, srcChildFile.name + File.separator)) } } } } @Throws(IOException::class) - private fun encrypt(srcFile: File, destFile: File) { + private fun encrypt(context: Context, srcFile: File, destFile: File) { + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_encrypting_script), + "${srcFile.path} -> ${destFile.path}", + ) destFile.outputStream().use { os -> writeHeader(os, JavaScriptFileSource(srcFile).executionMode.toShort()) AdvancedEncryptionStandard(mKey!!.toByteArray(), mInitVector!!) @@ -136,34 +251,93 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } } - private fun encryptToDir(srcFile: File, destDirFile: File) { + private fun encryptToDir(context: Context, srcFile: File, destDirFile: File) { val destFile = File(destDirFile, srcFile.name) - encrypt(srcFile, destFile) + encrypt(context, srcFile, destFile) } @Throws(IOException::class) - fun replaceFile(srcPath: String, relativeDestPath: String) = replaceFile(File(srcPath), relativeDestPath) + fun replaceFile(context: Context, srcPath: String, relativeDestPath: String) = replaceFile(context, File(srcPath), relativeDestPath) @Throws(IOException::class) - fun replaceFile(srcFile: File, relativeDestPath: String) = also { + fun replaceFile(context: Context, srcFile: File, relativeDestPath: String) = also { + ensureNotCancelled() val destFile = File(buildPath, relativeDestPath) if (destFile.name.endsWith(JAVASCRIPT.extensionWithDot)) { - encrypt(srcFile, destFile) + encrypt(context, srcFile, destFile) } else { + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_replacing_file), + "${srcFile.path} -> ${destFile.path}", + ) srcFile.copyTo(destFile, true) } } @Throws(IOException::class) - fun withConfig(config: ProjectConfig) = also { + fun withConfig(context: Context, config: ProjectConfig) = also { + notifyStepChanged(ProgressStep.BUILD) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_preparing_build_config), + ) config.also { mProjectConfig = it }.run { + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_reading_splash_resources), + mResourcesArscFile.path, + ) retrieveSplashThemeResources(launchConfig) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_configuring_manifest), + mManifestFile.path, + ) prepareManifestConfiguration(this) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_configuring_package_name), + packageName, + ) setArscPackageName(packageName) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_updating_project_config), + ) updateProjectConfig(this) - copyAssetsRecursively("", File(buildPath, "assets")) - copyLibrariesByConfig(this) - setScriptFile(sourcePath) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_assets_to), + File(buildPath, "assets").path, + ) + copyAssetsRecursively(context, "", File(buildPath, "assets")) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_copying_native_libraries), + ) + copyLibrariesByConfig(context, this) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing_source), + sourcePath, + ) + setScriptFile(context, sourcePath) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_applying_binary_resources), + ) } } @@ -203,6 +377,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File fun editManifest(): ManifestEditor = ManifestEditorWithAuthorities(FileInputStream(mManifestFile)).also { mManifestEditor = it } private fun updateProjectConfig(config: ProjectConfig) { + ensureNotCancelled() // 这里为什么要有这样的一个方法? ( // 会不会是因为有些配置需要写入到文件中, 这些配置包括自增的版本号, 用户的选择或键入值等等 @@ -222,7 +397,8 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File ProjectConfig.fromProjectDir(config.sourcePath)?.let { sourceProjectConfig -> sourceProjectConfig .setBuildInfo(BuildInfo.generate(sourceProjectConfig.buildInfo.buildNumber + 1)) - File(ProjectConfig.configFileOfDir(config.sourcePath)).writeText(sourceProjectConfig.toJson(true)) + mPendingProjectConfigFile = File(ProjectConfig.configFileOfDir(config.sourcePath)) + mPendingProjectConfigJson = sourceProjectConfig.toJson(true) return@run sourceProjectConfig } } @@ -254,11 +430,46 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } } + // Commit project config changes only after a successful build. + // zh-CN: 仅在构建成功后提交项目配置变更. + fun commitProjectConfigIfNeeded(context: Context) = also { + val pendingFile = mPendingProjectConfigFile + val pendingJson = mPendingProjectConfigJson + if (pendingFile == null || pendingJson == null) { + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_processing), + context.getString(R.string.text_sign_stage_completed), + ) + return@also + } + ensureNotCancelled() + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_writing_project_config), + pendingFile.path, + ) + pendingFile.writeText(pendingJson) + mPendingProjectConfigFile = null + mPendingProjectConfigJson = null + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_processing), + context.getString(R.string.text_sign_stage_completed), + ) + } + @Throws(Exception::class) - fun build() = also { - mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onBuild(this) } } + fun build(context: Context) = also { + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_building_resources), + ) mProjectConfig.iconBitmapGetter?.let { callable -> runCatching { + ensureNotCancelled() val tableBlock = TableBlock.load(mResourcesArscFile) val packageName = "${GlobalAppContext.get().packageName}.inrt" val packageBlock = tableBlock.getOrCreatePackage(0x7f, packageName).also { @@ -273,17 +484,42 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File it.createNewFile() } } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_writing_app_icon), + file.path, + ) callable.call()?.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(file)) }.onFailure { throw RuntimeException(it) } } mManifestEditor?.let { + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_writing_manifest), + mManifestFile.path, + ) it.commit() it.writeTo(FileOutputStream(mManifestFile)) } - mArscPackageName?.let { buildArsc() } + mArscPackageName?.let { + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_writing_resources_arsc), + mResourcesArscFile.path, + ) + buildArsc() + } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_build_completed), + ) } - private fun copyAssetsRecursively(assetPath: String, targetFile: File) { + private fun copyAssetsRecursively(context: Context, assetPath: String, targetFile: File) { + ensureNotCancelled() if (targetFile.isFile && targetFile.exists()) return val list = mAssetManager.list(assetPath) ?: return if (list.isEmpty()) /* asset is a file */ { @@ -292,9 +528,14 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File return } } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_asset), + "assets/$assetPath -> ${targetFile.path}", + ) mAssetManager.open(assetPath).use { input -> FileOutputStream(targetFile.absolutePath).use { output -> - input.copyTo(output) + copyStreamWithCancel(input, output) output.flush() } } @@ -302,36 +543,80 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File if (mAssetsDirExcludes.any { assetPath.matches(Regex("$it(/[^/]+)*")) }) { return } + val displayPath = if (assetPath.isEmpty()) "/" else "/$assetPath" + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_preparing_assets_dir), + displayPath, + ) targetFile.delete() targetFile.mkdir() list.forEach { + ensureNotCancelled() val sourcePath = if (assetPath.isEmpty()) it else "$assetPath/$it" - copyAssetsRecursively(sourcePath, File(targetFile, it)) + copyAssetsRecursively(context, sourcePath, File(targetFile, it)) } } } @Throws(Exception::class) - fun sign() = also { - mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onSign(this) } } - val fos = FileOutputStream(outApkFile) - TinySign.sign(File(buildPath), fos) - fos.close() + fun sign(context: Context) = also { + ensureNotCancelled() + notifyStepChanged(ProgressStep.SIGN) + val workspaceDir = File(buildPath) + outApkFile.parentFile?.let { if (!it.exists()) it.mkdirs() } + val unsignedApkFile = outApkFile + val tmpOutputApk = File(outApkFile.parentFile ?: workspaceDir, "${outApkFile.name}.signed.tmp") + if (tmpOutputApk.exists()) { + tmpOutputApk.delete() + } + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_creating_unsigned_apk), + unsignedApkFile.path, + ) + try { + BufferedOutputStream(FileOutputStream(unsignedApkFile, false), 256 * 1024).use { fos -> + TinySign.sign(workspaceDir, fos) + fos.flush() + } + } catch (e: Exception) { + if (tmpOutputApk.exists() && !tmpOutputApk.delete()) { + Log.w(TAG, "Failed to delete temporary signed apk after unsigned apk creation failure: ${tmpOutputApk.path}") + } + if (e.hasNoSpaceLeft()) { + throw IOException("No space left on device while creating unsigned APK: ${unsignedApkFile.path}", e) + } + throw e + } + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_unsigned_apk_created), + unsignedApkFile.path, + ) val defaultKeyStoreFile = File(buildPath, "default_key_store.bks") - val tmpOutputApk = File(buildPath, "temp.apk") + if (mProjectConfig.keyStore == null) { - // Replace FileUtils.copyInputStreamToFile(...). - // zh-CN: 替换 FileUtils.copyInputStreamToFile(...). - defaultKeyStoreFile.parentFile?.let { if (!it.exists()) it.mkdirs() } - GlobalAppContext.get().assets.open("default_key_store.bks").use { input -> - FileOutputStream(defaultKeyStoreFile, false).use { output -> - input.copyTo(output, bufferSize = 16 * 1024) - output.fd.sync() + // Replace FileUtils.copyInputStreamToFile(...). + // zh-CN: 替换 FileUtils.copyInputStreamToFile(...). + ensureNotCancelled() + defaultKeyStoreFile.parentFile?.let { if (!it.exists()) it.mkdirs() } + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_preparing_keystore), + defaultKeyStoreFile.path, + ) + GlobalAppContext.get().assets.open("default_key_store.bks").use { input -> + FileOutputStream(defaultKeyStoreFile, false).use { output -> + copyStreamWithCancel(input, output) + output.fd.sync() + } } } - val signer = ApkSigner(outApkFile, tmpOutputApk).apply { + ensureNotCancelled() + val signer = ApkSigner(unsignedApkFile, tmpOutputApk).apply { useDefaultSignatureVersion = false v1SigningEnabled = "V1" in mProjectConfig.signatureScheme v2SigningEnabled = "V2" in mProjectConfig.signatureScheme @@ -350,24 +635,84 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File alias = it.alias aliasPassword = AESUtils.decrypt(it.aliasPassword) } + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_using_keystore), + keyStoreFile.path, + ) // Re-sign using ApkSigner. // zh-CN: 使用 ApkSigner 重新签名. - if (!signer.signRelease(keyStoreFile, password, alias, aliasPassword)) { - throw java.lang.RuntimeException("Failed to re-sign using ApkSigner") + ensureNotCancelled() + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_processing), + context.getString(R.string.text_re_signing_apk), + ) + try { + if (!signer.signRelease(keyStoreFile, password, alias, aliasPassword)) { + throw RuntimeException("Failed to re-sign using ApkSigner") + } + } catch (e: Exception) { + if (tmpOutputApk.exists() && !tmpOutputApk.delete()) { + Log.w(TAG, "Failed to delete temporary signed apk after re-sign failure: ${tmpOutputApk.path}") + } + if (e.hasNoSpaceLeft()) { + throw IOException("No space left on device while re-signing APK: ${tmpOutputApk.path}", e) + } + throw e } try { - outApkFile.parentFile?.let { if (!it.exists()) it.mkdirs() } - tmpOutputApk.copyTo(outApkFile, overwrite = true) - } catch (e: java.lang.Exception) { - throw java.lang.RuntimeException(e) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_writing_signed_apk), + outApkFile.path, + ) + if (outApkFile.exists() && !outApkFile.delete()) { + throw IOException("Failed to delete unsigned apk before replace: ${outApkFile.path}") + } + if (!tmpOutputApk.renameTo(outApkFile)) { + copyFileWithLargeBuffer(tmpOutputApk, outApkFile) + } + } catch (e: Exception) { + if (e.hasNoSpaceLeft()) { + throw IOException("No space left on device while writing signed APK: ${outApkFile.path}", e) + } + throw RuntimeException(e) + } finally { + if (tmpOutputApk.exists() && !tmpOutputApk.delete()) { + Log.w(TAG, "Failed to delete temporary signed apk: ${tmpOutputApk.path}") + } } + notifyStepProgress( + ProgressStep.SIGN, + context.getString(R.string.text_sign_completed), + outApkFile.path, + ) } - fun cleanWorkspace() = also { - mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onClean(this) } } - delete(File(buildPath)) + fun cleanWorkspace(context: Context) = also { + notifyStepChanged(ProgressStep.CLEAN) + val workspace = File(buildPath) + val totalTargets = countDeleteTargets(workspace).coerceAtLeast(1) + val deletedTargets = intArrayOf(0) + notifyStepProgress( + ProgressStep.CLEAN, + context.getString(R.string.text_cleaning_workspace), + workspace.path, + ) + deleteWithProgress(context, workspace, totalTargets, deletedTargets) + notifyStepProgress( + ProgressStep.CLEAN, + context.getString(R.string.text_clean_completed), + workspace.path, + ) + } + + fun finish() = also { + mProgressCallback?.let { callback -> GlobalAppContext.post { callback.onFinished(this) } } } @Throws(IOException::class) @@ -377,23 +722,86 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File private fun buildArsc() { 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) + BufferedInputStream(FileInputStream(oldArsc), 256 * 1024).use { input -> + BufferedOutputStream(FileOutputStream(newArsc, false), 256 * 1024).use { output -> + val decoder = ARSCDecoder(input, null, false) + decoder.CloneArsc(output, mArscPackageName, true) + output.flush() + } + } oldArsc.delete() - newArsc.renameTo(oldArsc) + if (!newArsc.renameTo(oldArsc)) { + copyFileWithLargeBuffer(newArsc, oldArsc) + newArsc.delete() + } } - private fun delete(file: File) { - file.apply { if (isDirectory) listFiles()?.forEach { delete(it) } }.also { it.delete() } + private fun copyFileWithLargeBuffer(source: File, target: File, bufferSize: Int = 256 * 1024) { + FileInputStream(source).use { input -> + FileOutputStream(target, false).use { output -> + val buffer = ByteArray(bufferSize) + var len: Int + while (input.read(buffer).also { len = it } > 0) { + ensureNotCancelled() + output.write(buffer, 0, len) + } + } + } + } + + private fun Throwable.hasNoSpaceLeft(): Boolean { + var current: Throwable? = this + while (current != null) { + val message = current.message.orEmpty() + if (message.contains("ENOSPC", ignoreCase = true) || message.contains("No space left on device", ignoreCase = true)) { + return true + } + current = current.cause + } + return false + } + + private fun countDeleteTargets(file: File): Int { + if (!file.exists()) { + return 0 + } + return if (file.isDirectory) { + 1 + (file.listFiles()?.sumOf(::countDeleteTargets) ?: 0) + } else { + 1 + } + } + + private fun deleteWithProgress(context: Context, file: File, totalTargets: Int, deletedTargets: IntArray) { + ensureNotCancelled() + if (file.isDirectory) { + file.listFiles()?.forEach { child -> + deleteWithProgress(context, child, totalTargets, deletedTargets) + } + } + notifyStepProgress( + ProgressStep.CLEAN, + context.getString(R.string.text_deleting), + file.path, + ) + file.delete() + deletedTargets[0] += 1 + } + + enum class ProgressStep { + PREPARE, + BUILD, + SIGN, + CLEAN, } interface ProgressCallback { - fun onPrepare(builder: ApkBuilder) fun onBuild(builder: ApkBuilder) fun onSign(builder: ApkBuilder) fun onClean(builder: ApkBuilder) - + fun onStepProgress(builder: ApkBuilder, title: String, detail: String?) + fun onFinished(builder: ApkBuilder) } private inner class ManifestEditorWithAuthorities(manifestInputStream: InputStream?) : ManifestEditor(manifestInputStream) { @@ -422,7 +830,8 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } } - private fun copyLibrariesByConfig(config: ProjectConfig) { + private fun copyLibrariesByConfig(context: Context, config: ProjectConfig) { + ensureNotCancelled() // @Hint by SuperMonster003 on Dec 11, 2023. // ! The list contains only abi names not matching the canonical name itself. @@ -434,47 +843,78 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File // Try extracting native libraries from installed plugin APKs if needed. // zh-CN: 如有需要, 尝试从已安装插件 APK 中解压 native 库文件. - ensureAndExtractPluginLibrariesIfNeeded(config, potentialAbiAliasList) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_processing), + context.getString(R.string.text_resolving_plugin_native_libraries), + ) + ensureAndExtractPluginLibrariesIfNeeded(context, config, potentialAbiAliasList) config.abis.forEach { abiCanonicalName -> - copyLibrariesByAbi(abiCanonicalName, abiCanonicalName) + ensureNotCancelled() + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_libraries_for_abi), + abiCanonicalName, + ) + copyLibrariesByAbi(context, abiCanonicalName, abiCanonicalName) potentialAbiAliasList[abiCanonicalName]?.let { abiAliasName -> - copyLibrariesByAbi(abiAliasName, abiCanonicalName) + copyLibrariesByAbi(context, abiAliasName, abiCanonicalName) } } } private fun ensureAndExtractPluginLibrariesIfNeeded( + context: Context, config: ProjectConfig, potentialAbiAliasList: Map, ) { + ensureNotCancelled() Lib.entries.mapNotNull { if (it.isPlugin && config.libs.contains(it.label)) it.toPluginPair() else null }.forEach { (lib, plugin) -> + ensureNotCancelled() // Select plugin service by variant. // zh-CN: 通过 variant 选择插件服务. val (serviceInfo, selectedVariant) = selectPluginServiceOrThrow( lib = lib, action = plugin.action, ) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_selected_plugin), + "${lib.label} (${selectedVariant.variant}) from ${serviceInfo.packageName}", + ) Log.i(TAG, "Selected ${lib.label} plugin: variant=${selectedVariant.variant}, pkg=${serviceInfo.packageName}") // Extract libraries from installed plugin APK (variant-aware). // zh-CN: 从已安装插件 APK 中解压 so 文件, 并按变体裁剪. extractLibrariesFromPluginApkOrThrow( + context = context, config = config, requiredLibNames = selectedVariant.libsToInclude, serviceInfo = serviceInfo, potentialAbiAliasList = potentialAbiAliasList, ) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_extracted_plugin_libraries), + lib.label, + ) // Extract assets (models/labels) from installed plugin APK (variant-aware). // zh-CN: 从已安装插件 APK 中解压 assets 资源 (models/labels), 并按变体裁剪. extractAssetsFromPluginApkOrThrow( + context = context, serviceInfo = serviceInfo, pluginLibVariant = selectedVariant, ) + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_extracted_plugin_assets), + lib.label, + ) } } @@ -482,6 +922,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File lib: Lib, action: String, ): Pair { + ensureNotCancelled() val pm = globalContext.packageManager val moduleLabel = lib.label val pluginPair = lib.toPluginPair() @@ -526,6 +967,7 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } private fun queryPluginInfoBlocking(context: Context, serviceInfo: ServiceInfo, pluginPair: Pair): PluginLibVariant { + ensureNotCancelled() // Bind service and call getInfo() synchronously (packaging-time only). // zh-CN: 同步绑定服务并调用 getInfo() (仅打包阶段使用). val latch = CountDownLatch(1) @@ -586,9 +1028,11 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } private fun extractAssetsFromPluginApkOrThrow( + context: Context, serviceInfo: ServiceInfo, pluginLibVariant: PluginLibVariant, ) { + ensureNotCancelled() val pm = globalContext.packageManager val appInfo = getApplicationInfoCompat(pm, serviceInfo.packageName) @@ -603,7 +1047,9 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File // zh-CN: 解压必需前缀. val missingRequired = mutableListOf() requiredPrefixes.forEach { prefix -> + ensureNotCancelled() val ok = extractAssetsByPrefixFromApks( + context = context, apkPaths = apkPaths, assetPrefix = prefix, ) @@ -620,7 +1066,9 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File // Extract optional prefixes (best-effort). // zh-CN: 解压可选前缀 (尽力而为). optionalPrefixes.forEach { prefix -> + ensureNotCancelled() extractAssetsByPrefixFromApks( + context = context, apkPaths = apkPaths, assetPrefix = prefix, ) @@ -628,15 +1076,19 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } private fun extractAssetsByPrefixFromApks( + context: Context, apkPaths: List, assetPrefix: String, ): Boolean { + ensureNotCancelled() var extractedAny = false apkPaths.forEach { apkPath -> + ensureNotCancelled() runCatching { ZipFile(apkPath).use { zip -> val entries = zip.entries() while (entries.hasMoreElements()) { + ensureNotCancelled() val entry: ZipEntry = entries.nextElement() val name = entry.name if (!name.startsWith(assetPrefix)) continue @@ -646,10 +1098,15 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File val outFile = File(buildPath, "assets/$relative").apply { parentFile?.let { parent -> if (!parent.exists()) parent.mkdirs() } } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_extracting_plugin_asset), + "$apkPath!/$name -> ${outFile.path}", + ) zip.getInputStream(entry).use { input -> FileOutputStream(outFile, false).use { output -> - input.copyTo(output, bufferSize = 16 * 1024) + copyStreamWithCancel(input, output) output.fd.sync() } } @@ -667,11 +1124,13 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } private fun extractLibrariesFromPluginApkOrThrow( + context: Context, config: ProjectConfig, requiredLibNames: List, serviceInfo: ServiceInfo, potentialAbiAliasList: Map, ) { + ensureNotCancelled() val pm = globalContext.packageManager val appInfo = getApplicationInfoCompat(pm, serviceInfo.packageName) @@ -683,13 +1142,16 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File val missingPairs = mutableListOf>() // (abi, soName) config.abis.forEach { abiCanonicalName -> + ensureNotCancelled() val abiCandidates = buildList { add(abiCanonicalName) potentialAbiAliasList[abiCanonicalName]?.let { add(it) } }.distinct() requiredLibNames.forEach { soName -> + ensureNotCancelled() val ok = extractFirstMatchedSoFromApks( + context = context, apkPaths = apkPaths, abiCandidates = abiCandidates, soName = soName, @@ -718,23 +1180,32 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } private fun extractFirstMatchedSoFromApks( + context: Context, apkPaths: List, abiCandidates: List, soName: String, abiDestName: String, ): Boolean { + ensureNotCancelled() apkPaths.forEach { apkPath -> + ensureNotCancelled() runCatching { ZipFile(apkPath).use { zip -> abiCandidates.forEach { abiInApk -> + ensureNotCancelled() val entryName = "lib/$abiInApk/$soName" val entry = zip.getEntry(entryName) ?: return@forEach val outFile = File(buildPath, "lib/$abiDestName/$soName").apply { parentFile?.let { parent -> if (!parent.exists()) parent.mkdirs() } } + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_extracting_plugin_so), + "$apkPath!/$entryName -> ${outFile.path}", + ) zip.getInputStream(entry).use { input -> FileOutputStream(outFile, false).use { output -> - input.copyTo(output, bufferSize = 16 * 1024) + copyStreamWithCancel(input, output) output.fd.sync() } } @@ -760,18 +1231,26 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File } } - private fun copyLibrariesByAbi(abiSrcName: String, abiDestName: String) { + private fun copyLibrariesByAbi(context: Context, abiSrcName: String, abiDestName: String) { + ensureNotCancelled() // @Reference to LZX284 (https://github.com/LZX284) by SuperMonster003 on Dec 11, 2023. // ! http://pr.autojs6.com/187/files#diff-d932ac49867d4610f8eeb21b59306e8e923d016cbca192b254caebd829198856R61 val srcLibDir = File(appApkFile.parent, LIBRARY_DIR).path mLibsIncludes.distinct().forEach { libName -> + ensureNotCancelled() runCatching { - File(srcLibDir, "$abiSrcName/$libName").takeIf { it.exists() }?.copyTo( - File(buildPath, "lib/$abiDestName/$libName"), - overwrite = true - ) + val srcFile = File(srcLibDir, "$abiSrcName/$libName") + val destFile = File(buildPath, "lib/$abiDestName/$libName") + srcFile.takeIf { it.exists() }?.let { + notifyStepProgress( + ProgressStep.BUILD, + context.getString(R.string.text_copying_library), + "${srcFile.path} -> ${destFile.path}", + ) + it.copyTo(destFile, overwrite = true) + } }.onFailure { it.printStackTrace() } } } diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkPackager.java b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkPackager.java index 3aae00bb..cbbc0232 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/ApkPackager.java +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/ApkPackager.java @@ -4,12 +4,16 @@ import android.text.TextUtils; import org.autojs.autojs.apkbuilder.util.StreamUtils; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -20,8 +24,9 @@ import pxb.android.tinysign.TinySign; */ public class ApkPackager { - private InputStream mApkInputStream; - private String mWorkspacePath; + private final InputStream mApkInputStream; + private final String mWorkspacePath; + private AtomicBoolean mCancelSignal; public ApkPackager(InputStream apkInputStream, String workspacePath) { mApkInputStream = apkInputStream; @@ -33,20 +38,47 @@ public class ApkPackager { mWorkspacePath = workspacePath; } + public ApkPackager setCancelSignal(AtomicBoolean cancelSignal) { + mCancelSignal = cancelSignal; + return this; + } + + // Throw early when cancellation is requested to avoid partial outputs. + // zh-CN: 当收到取消请求时尽早抛出, 以避免产生部分输出. + private void ensureNotCancelled() { + if (mCancelSignal != null && mCancelSignal.get()) { + throw new CancellationException("Build aborted"); + } + if (Thread.currentThread().isInterrupted()) { + throw new CancellationException("Build aborted"); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") public void unzip() throws IOException { - ZipInputStream zis = new ZipInputStream(mApkInputStream); - for (ZipEntry e = zis.getNextEntry(); e != null; e = zis.getNextEntry()) { - String name = e.getName(); - if (!e.isDirectory() && !TextUtils.isEmpty(name)) { - File file = new File(mWorkspacePath, name); - System.out.println(file); - file.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(file); - StreamUtils.write(zis, fos); - fos.close(); + // Buffer IO to reduce syscall overhead during APK extraction. + // zh-CN: 使用缓冲 IO 以减少 APK 解压过程中的系统调用开销. + try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(mApkInputStream, StreamUtils.DEFAULT_BUFFER_SIZE))) { + for (ZipEntry e = zis.getNextEntry(); e != null; e = zis.getNextEntry()) { + ensureNotCancelled(); + String name = e.getName(); + if (!e.isDirectory() && !TextUtils.isEmpty(name)) { + File file = new File(mWorkspacePath, name); + File parentFile = file.getParentFile(); + if (parentFile != null) { + parentFile.mkdirs(); + } + try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file), StreamUtils.DEFAULT_BUFFER_SIZE)) { + byte[] buffer = new byte[StreamUtils.DEFAULT_BUFFER_SIZE]; + int len; + while ((len = zis.read(buffer)) > 0) { + ensureNotCancelled(); + bos.write(buffer, 0, len); + } + } + } } } - zis.close(); } public void repackage(String newApkPath) throws Exception { diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/util/StreamUtils.java b/app/src/main/java/org/autojs/autojs/apkbuilder/util/StreamUtils.java index ae434822..5b495165 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/util/StreamUtils.java +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/util/StreamUtils.java @@ -10,8 +10,10 @@ import java.io.OutputStream; */ public class StreamUtils { + public static final int DEFAULT_BUFFER_SIZE = 16 * 1024; + public static void write(InputStream inputStream, OutputStream out) throws IOException { - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int len; while ((len = inputStream.read(buffer)) > 0) { out.write(buffer, 0, len); @@ -20,7 +22,7 @@ public class StreamUtils { public static byte[] readAsBytes(InputStream inputStream) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[4096]; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int len; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); 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 0aa08201..f04a60ad 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 @@ -4,8 +4,10 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.SystemClock; import android.text.Editable; import android.text.TextUtils; import android.text.util.Linkify; @@ -24,6 +26,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.lifecycle.ViewModelProvider; +import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.textfield.TextInputLayout; @@ -35,14 +38,15 @@ import net.dongliu.apk.parser.bean.ApkMeta; import org.autojs.autojs.apkbuilder.ApkBuilder; import org.autojs.autojs.apkbuilder.keystore.KeyStore; import org.autojs.autojs.core.pref.Language; -import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.external.fileprovider.AppFileProvider; import org.autojs.autojs.model.explorer.Explorers; import org.autojs.autojs.model.script.ScriptFile; +import org.autojs.autojs.pio.PFiles; import org.autojs.autojs.project.ProjectConfig; import org.autojs.autojs.runtime.api.AppUtils; import org.autojs.autojs.runtime.api.AppUtils.Companion.SimpleVersionInfo; import org.autojs.autojs.runtime.api.augment.pinyin.Pinyin; +import org.autojs.autojs.theme.ThemeColorManager; import org.autojs.autojs.ui.BaseActivity; import org.autojs.autojs.ui.common.NotAskAgainDialog; import org.autojs.autojs.ui.error.ErrorDialogActivity; @@ -55,13 +59,17 @@ import org.autojs.autojs.ui.widget.RoundCheckboxWithText; import org.autojs.autojs.util.AndroidUtils; import org.autojs.autojs.util.AndroidUtils.Abi; import org.autojs.autojs.util.BitmapUtils; +import org.autojs.autojs.util.ColorUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.util.EnvironmentUtils; import org.autojs.autojs.util.IntentUtils; import org.autojs.autojs.util.IntentUtils.ToastExceptionHolder; +import org.autojs.autojs.util.StringUtils; import org.autojs.autojs.util.ViewUtils; import org.autojs.autojs.util.WorkingDirectoryUtils; import org.autojs.autojs6.R; import org.autojs.autojs6.databinding.ActivityBuildBinding; +import org.autojs.autojs6.databinding.DialogBuildProgressBinding; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -70,12 +78,14 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.regex.Pattern; @@ -89,7 +99,7 @@ import static org.autojs.autojs.util.StringUtils.key; * Created by Stardust on Oct 22, 2017. * Modified by SuperMonster003 as of Jan 6, 2026. * - * @noinspection ResultOfMethodCallIgnored + * @noinspection ResultOfMethodCallIgnored, unused */ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCallback { @@ -224,6 +234,35 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa private ProjectConfig mProjectConfig; private MaterialDialog mProgressDialog; + private ImageView mProgressPrepareIcon; + private TextView mProgressPrepareText; + private ImageView mProgressBuildIcon; + private TextView mProgressBuildText; + private ImageView mProgressSignIcon; + private TextView mProgressSignText; + private ImageView mProgressCleanIcon; + private TextView mProgressCleanText; + private TextView mProgressPrepareDurationText; + private TextView mProgressBuildDurationText; + private TextView mProgressSignDurationText; + private TextView mProgressCleanDurationText; + private TextView mStateTitleText; + private TextView mStateContentText; + private final EnumMap mStepDurationMs = new EnumMap<>(BuildStep.class); + @Nullable + private BuildStep mCurrentTimingStep; + private long mCurrentTimingStepStartedAtMs; + // Track cancellation state for cooperative build abort. + // zh-CN: 跟踪取消状态, 用于协作式中止构建. + private final AtomicBoolean mBuildCancelled = new AtomicBoolean(false); + @Nullable + private File mBuildWorkspace; + @Nullable + private File mBuildOutputApk; + @Nullable + private File mBuildOutputApkBackup; + @Nullable + private Thread mBuildThread; private String mSource; private boolean mIsDefaultIcon = true; private boolean mIsProjectLevelBuilding; @@ -712,7 +751,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa } @Override - protected void onNewIntent(Intent intent) { + protected void onNewIntent(@NonNull Intent intent) { super.onNewIntent(intent); } @@ -910,18 +949,40 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa block == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS; } - @SuppressWarnings("ResultOfMethodCallIgnored") @SuppressLint("CheckResult") private void doBuildingApk() { ProjectConfig projectConfig = determineProjectConfig(); File buildPath = new File(getCacheDir(), "build/"); File outApk = new File(mOutputPathView.getText().toString(), String.format("%s_v%s.apk", projectConfig.getName(), projectConfig.getVersionName())); + File outApkBackup = null; + try { + outApkBackup = backupExistingOutputApkIfNeeded(outApk); + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to backup existing output apk", e); + ErrorDialogActivity.showErrorDialog(this, R.string.text_failed_to_build, e.getMessage()); + return; + } + mBuildCancelled.set(false); + mBuildWorkspace = buildPath; + mBuildOutputApk = outApk; + mBuildOutputApkBackup = outApkBackup; showProgressDialog(); - Observable.fromCallable(() -> callApkBuilder(buildPath, outApk, projectConfig)) + Observable.fromCallable(() -> { + mBuildThread = Thread.currentThread(); + try { + return callApkBuilder(buildPath, outApk, projectConfig); + } finally { + mBuildThread = null; + } + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(apkBuilder -> { + if (mBuildCancelled.get()) { + handleBuildAborted(); + return; + } if (apkBuilder != null) { onBuildSuccessful(outApk); } else { @@ -930,6 +991,34 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa }, this::onBuildFailed); } + @Nullable + private File backupExistingOutputApkIfNeeded(@NonNull File outApk) throws IOException { + if (!outApk.exists()) { + return null; + } + File parentDir = outApk.getAbsoluteFile().getParentFile(); + if (parentDir == null) { + throw new IOException("Invalid output apk path: " + outApk.getPath()); + } + File backupApk = new File(parentDir, outApk.getName() + "." + System.nanoTime() + ".bak"); + if (backupApk.exists() && !backupApk.delete()) { + throw new IOException("Failed to remove stale output apk backup: " + backupApk.getPath()); + } + if (outApk.renameTo(backupApk)) { + return backupApk; + } + if (!PFiles.copy(outApk.getPath(), backupApk.getPath())) { + throw new IOException("Failed to backup existing output apk: " + outApk.getPath()); + } + if (!outApk.delete()) { + if (!backupApk.delete()) { + Log.w(LOG_TAG, "Failed to delete incomplete backup apk after backup cleanup failure: " + backupApk.getPath()); + } + throw new IOException("Failed to remove original output apk after backup copy: " + outApk.getPath()); + } + return backupApk; + } + private ProjectConfig determineProjectConfig() { ArrayList abis = collectCheckedItems(mFlexboxAbisView); ArrayList libs = collectCheckedItems(mFlexboxLibsView); @@ -988,71 +1077,362 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa InputStream templateApk = getAssets().open(TEMPLATE_APK_NAME); return new ApkBuilder(templateApk, outApk, buildPath.getPath()) .setProgressCallback(BuildActivity.this) - .prepare() - .withConfig(projectConfig) - .build() - .sign() - .cleanWorkspace(); + .setCancelSignal(mBuildCancelled) + .prepare(BuildActivity.this) + .withConfig(BuildActivity.this, projectConfig) + .build(BuildActivity.this) + .sign(BuildActivity.this) + .commitProjectConfigIfNeeded(BuildActivity.this) + .cleanWorkspace(BuildActivity.this) + .finish(); + } + + private enum BuildStep { + PREPARE, + BUILD, + SIGN, + CLEAN, + FINISHED, } private void showProgressDialog() { + dismissProgressDialog(); + View contentView = DialogBuildProgressBinding.inflate(getLayoutInflater()).getRoot(); + mProgressPrepareIcon = contentView.findViewById(R.id.icon_prepare); + mProgressPrepareText = contentView.findViewById(R.id.text_prepare); + mProgressBuildIcon = contentView.findViewById(R.id.icon_build); + mProgressBuildText = contentView.findViewById(R.id.text_build); + mProgressSignIcon = contentView.findViewById(R.id.icon_sign); + mProgressSignText = contentView.findViewById(R.id.text_sign); + mProgressCleanIcon = contentView.findViewById(R.id.icon_clean); + mProgressCleanText = contentView.findViewById(R.id.text_clean); + mProgressPrepareDurationText = contentView.findViewById(R.id.text_prepare_duration); + mProgressBuildDurationText = contentView.findViewById(R.id.text_build_duration); + mProgressSignDurationText = contentView.findViewById(R.id.text_sign_duration); + mProgressCleanDurationText = contentView.findViewById(R.id.text_clean_duration); + mStateTitleText = contentView.findViewById(R.id.text_state_title); + mStateContentText = contentView.findViewById(R.id.text_state_content); + resetStepDurations(); + applyThemeColorIcons(mProgressPrepareIcon, mProgressBuildIcon, mProgressSignIcon, mProgressCleanIcon); mProgressDialog = new MaterialDialog.Builder(this) - .progress(true, 100) - .content(R.string.text_in_progress) + .title(R.string.text_building_apk) + .customView(contentView, false) + .neutralText("") + .negativeText("") + .positiveText(R.string.dialog_button_abort) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive((dialog, which) -> requestBuildAbort()) + .autoDismiss(false) .cancelable(false) .show(); + updateProgressDialog(BuildStep.PREPARE); + mStateTitleText.setText(getString(R.string.text_property_colon, getString(R.string.text_processing))); + } + + private void applyThemeColorIcons(ImageView... imageView) { + int backgroundColor = getColor(R.color.window_background); + int adjustedColor = ColorUtils.adjustColorForContrast(backgroundColor, ThemeColorManager.getColorPrimary(), 2.3); + for (ImageView iv : imageView) { + iv.setImageTintList(ColorStateList.valueOf(adjustedColor)); + } + } + + private void updateProgressDialog(BuildStep currentStep) { + if (mProgressDialog == null) { + return; + } + recordStepDuration(currentStep); + setProgressStep(mProgressPrepareIcon, mProgressPrepareText, mProgressPrepareDurationText, BuildStep.PREPARE, currentStep); + setProgressStep(mProgressBuildIcon, mProgressBuildText, mProgressBuildDurationText, BuildStep.BUILD, currentStep); + setProgressStep(mProgressSignIcon, mProgressSignText, mProgressSignDurationText, BuildStep.SIGN, currentStep); + setProgressStep(mProgressCleanIcon, mProgressCleanText, mProgressCleanDurationText, BuildStep.CLEAN, currentStep); + } + + private void setProgressStep(@Nullable ImageView iconView, @Nullable TextView stateView, @Nullable TextView durationView, BuildStep step, BuildStep currentStep) { + if (step.ordinal() < currentStep.ordinal()) { + if (iconView != null) iconView.setImageResource(R.drawable.ic_check_mark); + if (stateView != null) stateView.setText(ensureTextEndsWithDot(stateView.getText())); + if (durationView != null && currentStep.ordinal() - step.ordinal() == 1) { + updateStepDurationText(durationView, step, SystemClock.elapsedRealtime()); + } + } else if (step == currentStep) { + if (iconView != null) iconView.setImageResource(R.drawable.ic_right_arrow); + if (stateView != null) stateView.setText(ensureTextEndsWithHalfEllipsis(stateView.getText())); + } else { + if (iconView != null) iconView.setImageResource(R.drawable.transparent); + if (stateView != null) stateView.setText(ensureTextEndsWithoutDot(stateView.getText())); + } + } + + private void resetStepDurations() { + mStepDurationMs.clear(); + mCurrentTimingStep = null; + mCurrentTimingStepStartedAtMs = 0L; + } + + private void recordStepDuration(@NonNull BuildStep currentStep) { + long now = SystemClock.elapsedRealtime(); + if (mCurrentTimingStep != null && mCurrentTimingStep != currentStep && isTrackedStep(mCurrentTimingStep)) { + mStepDurationMs.put(mCurrentTimingStep, Math.max(0L, now - mCurrentTimingStepStartedAtMs)); + } + if (isTrackedStep(currentStep) && !mStepDurationMs.containsKey(currentStep)) { + if (mCurrentTimingStep != currentStep) { + mCurrentTimingStep = currentStep; + mCurrentTimingStepStartedAtMs = now; + } + } else if (!isTrackedStep(currentStep)) { + mCurrentTimingStep = null; + mCurrentTimingStepStartedAtMs = 0L; + } + } + + private boolean isTrackedStep(@NonNull BuildStep step) { + return step == BuildStep.PREPARE + || step == BuildStep.BUILD + || step == BuildStep.SIGN + || step == BuildStep.CLEAN; + } + + @Nullable + private Long resolveStepDurationMs(@NonNull BuildStep step, long now) { + Long duration = mStepDurationMs.get(step); + if (duration != null) { + return duration; + } + if (step == mCurrentTimingStep) { + return Math.max(0L, now - mCurrentTimingStepStartedAtMs); + } + return null; + } + + private void updateStepDurationText(@NonNull TextView textView, @NonNull BuildStep step, long now) { + Long durationMs = resolveStepDurationMs(step, now); + textView.setText(durationMs == null ? "" : formatStepDuration(durationMs)); + } + + @NonNull + private String formatStepDuration(long durationMs) { + long safeDurationMs = Math.max(0L, durationMs); + long tenths = Math.round(safeDurationMs / 100.0d); + long integerPart = tenths / 10; + long decimalPart = tenths % 10; + // if (decimalPart == 0L) { + // return "[ " + integerPart + " s ]"; + // } + return "[ " + integerPart + "." + decimalPart + " s ]"; + } + + private String ensureTextEndsWithDot(CharSequence text) { + return ensureTextEndsWithoutDot(text) + "."; + } + + private String ensureTextEndsWithHalfEllipsis(CharSequence text) { + return ensureTextEndsWithoutDot(text) + StringUtils.str(R.string.text_half_ellipsis); + } + + private String ensureTextEndsWithoutDot(CharSequence text) { + var tmp = text.toString(); + while (tmp.length() > 0 && tmp.charAt(tmp.length() - 1) == '.') { + tmp = tmp.subSequence(0, tmp.length() - 1).toString(); + } + return tmp; + } + + // Abort build cooperatively and trigger cleanup as early as possible. + // zh-CN: 协作式中止构建, 并尽早触发清理. + private void requestBuildAbort() { + if (!mBuildCancelled.compareAndSet(false, true)) { + return; + } + if (mProgressDialog != null) { + mProgressDialog.setTitle(R.string.text_aborting); + View button = mProgressDialog.getActionButton(DialogAction.POSITIVE); + if (button != null) { + button.setEnabled(false); + } + } + Thread buildThread = mBuildThread; + if (buildThread != null) { + buildThread.interrupt(); + } + cleanupBuildArtifactsAsync(false); + } + + private void cleanupBuildArtifactsAsync(boolean restoreOutputApk) { + File workspace = mBuildWorkspace; + File outApk = mBuildOutputApk; + File outApkBackup = mBuildOutputApkBackup; + if (workspace == null && outApk == null && outApkBackup == null) { + return; + } + Schedulers.io().scheduleDirect(() -> cleanupBuildArtifacts(workspace, outApk, outApkBackup, restoreOutputApk)); + } + + // Best-effort cleanup for workspace and output artifacts. + // zh-CN: 对工作区和输出产物执行尽力清理. + private void cleanupBuildArtifacts(@Nullable File workspace, @Nullable File outApk, @Nullable File outApkBackup, boolean restoreOutputApk) { + if (workspace != null && workspace.exists()) { + PFiles.deleteRecursively(workspace); + } + if (outApk != null && outApk.exists()) { + outApk.delete(); + } + if (restoreOutputApk) { + restoreOutputApkIfNeeded(outApk, outApkBackup); + } + } + + private void restoreOutputApkIfNeeded(@Nullable File outApk, @Nullable File outApkBackup) { + if (outApk == null || outApkBackup == null || !outApkBackup.exists()) { + return; + } + if (outApk.exists() && !outApk.delete()) { + Log.w(LOG_TAG, "Failed to delete output apk before restore: " + outApk.getPath()); + } + if (!outApkBackup.renameTo(outApk)) { + if (!PFiles.copy(outApkBackup.getPath(), outApk.getPath())) { + Log.w(LOG_TAG, "Failed to restore output apk backup: " + outApk.getPath()); + return; + } + if (!outApkBackup.delete()) { + Log.w(LOG_TAG, "Failed to delete output apk backup: " + outApkBackup.getPath()); + } + } + } + + private void discardOutputApkBackup() { + File outApkBackup = mBuildOutputApkBackup; + if (outApkBackup != null && outApkBackup.exists() && !outApkBackup.delete()) { + Log.w(LOG_TAG, "Failed to delete output apk backup: " + outApkBackup.getPath()); + } + } + + private void handleBuildAborted() { + dismissProgressDialog(); + cleanupBuildArtifactsAsync(true); + ViewUtils.showToast(this, getString(R.string.text_operation_aborted)); + finishBuildState(); + } + + private void finishBuildState() { + mBuildWorkspace = null; + mBuildOutputApk = null; + mBuildOutputApkBackup = null; + mBuildThread = null; + mBuildCancelled.set(false); + } + + private void dismissProgressDialog() { + if (mProgressDialog == null) { + return; + } + mProgressDialog.dismiss(); + mProgressDialog = null; + mProgressPrepareIcon = null; + mProgressPrepareText = null; + mProgressBuildIcon = null; + mProgressBuildText = null; + mProgressSignIcon = null; + mProgressSignText = null; + mProgressCleanIcon = null; + mProgressCleanText = null; + mProgressPrepareDurationText = null; + mProgressBuildDurationText = null; + mProgressSignDurationText = null; + mProgressCleanDurationText = null; + mStateTitleText = null; + mStateContentText = null; + resetStepDurations(); } private void onBuildFailed(Throwable error) { - if (mProgressDialog != null) { - mProgressDialog.dismiss(); - mProgressDialog = null; + if (mBuildCancelled.get() || error instanceof CancellationException) { + handleBuildAborted(); + return; } + dismissProgressDialog(); + restoreOutputApkIfNeeded(mBuildOutputApk, mBuildOutputApkBackup); ErrorDialogActivity.showErrorDialog(this, R.string.text_failed_to_build, error.getMessage()); Log.e(LOG_TAG, "Failed to build", error); + finishBuildState(); } private void onBuildSuccessful(File outApk) { + if (mBuildCancelled.get()) { + handleBuildAborted(); + return; + } + discardOutputApkBackup(); Explorers.workspace().refreshAll(); - mProgressDialog.dismiss(); - mProgressDialog = null; - new MaterialDialog.Builder(this) - .title(R.string.text_build_succeeded) - .content(getString(R.string.format_build_succeeded, outApk.getPath())) - .positiveText(R.string.text_install) - .positiveColorRes(R.color.dialog_button_attraction) - .onPositive((dialog, which) -> IntentUtils.installApk( + // dismissProgressDialog(); + if (mProgressDialog != null) { + mProgressDialog.setTitle(R.string.text_build_succeeded); + mStateTitleText.setText(getString(R.string.text_property_colon, getString(R.string.text_built_apk_file_path))); + mStateContentText.setText(outApk.getPath()); + var positiveButton = mProgressDialog.getActionButton(DialogAction.POSITIVE); + positiveButton.setEnabled(true); + positiveButton.setText(getString(R.string.text_install)); + positiveButton.setTextColor(getColor(R.color.dialog_button_attraction)); + positiveButton.setOnClickListener(v -> { + IntentUtils.installApk( BuildActivity.this, outApk.getPath(), AppFileProvider.AUTHORITY, new ToastExceptionHolder(BuildActivity.this) - )) - .negativeText(R.string.text_cancel) - .negativeColorRes(R.color.dialog_button_default) - .neutralText(R.string.dialog_button_file_information) - .neutralColorRes(R.color.dialog_button_hint) - .onNeutral((dialog, which) -> ApkInfoDialogManager.showApkInfoDialog(dialog.getContext(), outApk)) - .show(); + ); + dismissProgressDialog(); + }); + var negativeButton = mProgressDialog.getActionButton(DialogAction.NEGATIVE); + negativeButton.setEnabled(true); + negativeButton.setText(getString(R.string.text_cancel)); + negativeButton.setTextColor(getColor(R.color.dialog_button_default)); + negativeButton.setOnClickListener(v -> { + dismissProgressDialog(); + }); + var neutralButton = mProgressDialog.getActionButton(DialogAction.NEUTRAL); + neutralButton.setEnabled(true); + neutralButton.setText(getString(R.string.dialog_button_file_information)); + neutralButton.setTextColor(getColor(R.color.dialog_button_hint)); + neutralButton.setOnClickListener(v -> { + ApkInfoDialogManager.showApkInfoDialog(this, outApk); + }); + } + finishBuildState(); } @Override public void onPrepare(@NonNull ApkBuilder builder) { - mProgressDialog.setContent(R.string.apk_builder_prepare); + updateProgressDialog(BuildStep.PREPARE); } @Override public void onBuild(@NonNull ApkBuilder builder) { - mProgressDialog.setContent(R.string.apk_builder_build); + updateProgressDialog(BuildStep.BUILD); } @Override public void onSign(@NonNull ApkBuilder builder) { - mProgressDialog.setContent(R.string.apk_builder_package); + updateProgressDialog(BuildStep.SIGN); } @Override public void onClean(@NonNull ApkBuilder builder) { - mProgressDialog.setContent(R.string.apk_builder_clean); + updateProgressDialog(BuildStep.CLEAN); + } + + @Override + public void onStepProgress(@NonNull ApkBuilder builder, @NonNull String title, @Nullable String detail) { + if (mStateTitleText != null) { + mStateTitleText.setText(getString(R.string.text_property_colon, title)); + } + if (!TextUtils.isEmpty(detail) && mStateContentText != null) { + mStateContentText.setText(detail); + } + } + + @Override + public void onFinished(@NotNull ApkBuilder builder) { + updateProgressDialog(BuildStep.FINISHED); } @SuppressWarnings("ResultOfMethodCallIgnored") diff --git a/app/src/main/res/drawable/ic_check_mark.png b/app/src/main/res/drawable/ic_check_mark.png new file mode 100644 index 0000000000000000000000000000000000000000..adf58b3a8741296a26622709803c6cd9bd003bba GIT binary patch literal 7261 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/dialog_build_progress.xml b/app/src/main/res/layout/dialog_build_progress.xml new file mode 100644 index 00000000..2e92e71c --- /dev/null +++ b/app/src/main/res/layout/dialog_build_progress.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index decd989a..ed9b85dd 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -9,11 +9,11 @@ - مبنى... - تنظيف... - التغليف ... + مبنى + تنظيف + التغليف نسخة غير متوافقة من \"APK Builder\".\nتحميله الآن؟ - خطة... + خطة SDK الجهاز حجم الملف مثبت @@ -536,7 +536,7 @@ توثيق AutoJs6 سجل سجل - مركز اضافات AutoJs6 + AutoJs6 الاضافات الاضافات AutoJs6 إعدادات إعدادات @@ -674,7 +674,7 @@ حذف المجلد حذف الخط هل تريد حذف هذه المراجعة نهائيا? - جارٍ الحذف... + جارٍ الحذف الوصف الغاء التحديد الوجهة @@ -1086,7 +1086,7 @@ لإغلاق، اضغط على \"رجوع\" أو \"خفض الصوت\" معاينة سجل العملية - يعالج + جارٍ المعالجة مشروع موقع المشروع وصول وسائل الإعلام المشروع @@ -1358,4 +1358,58 @@ الاضافة \"%1$s\" غير مُفوّضة في مركز الاضافات لم يتم العثور على الاضافة \"%1$s\" ضمن التطبيقات المثبتة طريقة اتصال الاضافة غير مدعومة + %1$s: + العنوان + المحتوى + جارٍ بناء APK + مسار APK المبني + تنظيف + توقيع + بناء + تحضير + جارٍ تحضير مساحة العمل + جارٍ استخراج APK القالب + اكتمل التحضير + جارٍ نسخ مجلد المشروع + جارٍ نسخ ملف السكربت + اكتملت معالجة المصدر + جارٍ نسخ المجلد + جارٍ نسخ الملف + جارٍ تشفير السكربت + جارٍ استبدال الملف + جارٍ تحضير اعدادات البناء + جارٍ قراءة موارد شاشة البدء + جارٍ ضبط manifest + جارٍ ضبط اسم الحزمة + جارٍ نسخ assets الى + جارٍ معالجة المصدر + جارٍ تطبيق الموارد الثنائية + جارٍ نسخ المكتبات الاصلية + جارٍ تحديث اعدادات المشروع + اكتملت مرحلة التوقيع + جارٍ كتابة اعدادات المشروع + جارٍ بناء الموارد + اكتمل البناء + جارٍ كتابة resources.arsc + جارٍ كتابة manifest + جارٍ كتابة ايقونة التطبيق + جارٍ نسخ asset + جارٍ تحضير مجلد assets + جارٍ نسخ المكتبة + جارٍ تحضير keystore + تم انشاء APK غير موقّع + جارٍ انشاء APK غير موقّع + جارٍ استخدام keystore + جارٍ اعادة توقيع APK + جارٍ كتابة APK الموقّع + اكتمل التوقيع + اكتمل التنظيف + جارٍ تنظيف مساحة العمل + جارٍ تحليل مكتبات الاضافة الاصلية + جارٍ نسخ المكتبات لـ ABI + الاضافة المحددة + تم استخراج مكتبات الاضافة + تم استخراج assets الاضافة + جارٍ استخراج asset الاضافة + جارٍ استخراج ملف so للاضافة \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 2d29911f..5fbad24f 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -4,11 +4,11 @@ - Building... - Cleaning... - Packaging... + Building + Cleaning + Packaging Incompatible version of \"apk builder\".\nDownload it now? - Preparing... + Preparing Device SDK File size Installed @@ -531,7 +531,7 @@ Docs AutoJs6 Log Log - AutoJs6 Plugin Center + AutoJs6 Plugins Plugins AutoJs6 Settings Settings @@ -669,7 +669,7 @@ Delete folder Delete line Delete this revision permanently? - Deleting... + Deleting Description Deselect all Destination @@ -772,7 +772,7 @@ File name File name: %1$s File does not exist - File Path + File path File \"%1$s\" is not an executable script Filename cannot be empty Filename cannot contain the following characters: \\ / : * ? " < > | @@ -1353,4 +1353,58 @@ Plugin \"%1$s\" is not authorized in Plugin Center Plugin \"%1$s\" not found in installed apps Unsupported plugin connection + %1$s: + Title + Content + Building APK + Built APK file path + Clean + Sign + Build + Prepare + Preparing workspace + Extracting template APK + Prepare completed + Copying project directory + Copying script file + Source processing completed + Copying directory + Copying file + Encrypting script + Replacing file + Preparing build config + Reading splash resources + Configuring manifest + Configuring package name + Copying assets to + Processing source + Applying binary resources + Copying native libraries + Updating project config + Sign stage completed + Writing project config + Building resources + Build completed + Writing resources.arsc + Writing manifest + Writing app icon + Copying asset + Preparing assets dir + Copying library + Preparing keystore + Unsigned APK created + Creating unsigned APK + Using keystore + Re-signing APK + Writing signed APK + Sign completed + Clean completed + Cleaning workspace + Resolving plugin native libraries + Copying libraries for ABI + Selected plugin + Extracted plugin libraries + Extracted plugin assets + Extracting plugin asset + Extracting plugin so \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e1b76104..53a7e20f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -7,11 +7,11 @@ - Construir... - Limpieza... - Empaquetando... + Construir + Limpieza + Empaquetando Versión incompatible de \"apk builder\".\n¿Descargarlo ahora? - Preparando... + Preparando SDK del dispositivo Tamaño de archivo Instalado @@ -534,7 +534,7 @@ Docs AutoJs6 Registrar Registrar - Centro de plugins de AutoJs6 + AutoJs6 Plugins Plugins AutoJs6 Configuración Configuración @@ -672,7 +672,7 @@ Eliminar carpeta Borrar línea ¿Eliminar esta revision de forma permanente? - Eliminando... + Eliminando Descripción Deseleccionar todo Destino @@ -1356,4 +1356,58 @@ El plugin \"%1$s\" no esta autorizado en el Centro de plugins No se encontro el plugin \"%1$s\" en las apps instaladas Conexion de plugin no compatible + %1$s: + Titulo + Contenido + Compilando APK + Ruta del APK generado + Limpiar + Firmar + Compilar + Preparar + Preparando el espacio de trabajo + Extrayendo APK plantilla + Preparacion completada + Copiando directorio del proyecto + Copiando archivo de script + Procesamiento del codigo fuente completado + Copiando directorio + Copiando archivo + Cifrando script + Reemplazando archivo + Preparando configuracion de compilacion + Leyendo recursos de inicio + Configurando manifest + Configurando nombre de paquete + Copiando assets a + Procesando codigo fuente + Aplicando recursos binarios + Copiando bibliotecas nativas + Actualizando configuracion del proyecto + Etapa de firma completada + Escribiendo configuracion del proyecto + Compilando recursos + Compilacion completada + Escribiendo resources.arsc + Escribiendo manifest + Escribiendo icono de la app + Copiando asset + Preparando dir de assets + Copiando biblioteca + Preparando keystore + APK sin firmar creado + Creando APK sin firmar + Usando keystore + Re-firmando APK + Escribiendo APK firmado + Firma completada + Limpieza completada + Limpiando el espacio de trabajo + Resolviendo bibliotecas nativas del plugin + Copiando bibliotecas para ABI + Plugin seleccionado + Bibliotecas del plugin extraidas + Assets del plugin extraidos + Extrayendo asset del plugin + Extrayendo archivo so del plugin \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 54a61ad3..0c83c364 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -7,11 +7,11 @@ - Construire... - Nettoyage... - Emballage... + Construire + Nettoyage + Emballage Version incompatible de \"apk builder\".\nTéléchargez-la maintenant ? - Préparation... + Préparation SDK de l\'appareil Taille du fichier Installé @@ -534,7 +534,7 @@ Docs AutoJs6 Journal Journal - Centre des plugins AutoJs6 + AutoJs6 Plugins Plugins AutoJs6 Réglages Réglages @@ -672,7 +672,7 @@ Supprimer le dossier Supprimer la ligne Supprimer definitivement cette revision? - Suppression... + Suppression Description Tout deselectionner Destination @@ -1356,4 +1356,58 @@ Le plugin \"%1$s\" n\'est pas autorise dans le Centre des plugins Plugin \"%1$s\" introuvable parmi les applis installees Connexion de plugin non prise en charge + %1$s : + Titre + Contenu + Compilation de l\'APK + Chemin de l\'APK genere + Nettoyer + Signer + Compiler + Preparer + Preparation de l\'espace de travail + Extraction de l\'APK modele + Preparation terminee + Copie du repertoire du projet + Copie du fichier de script + Traitement du code source termine + Copie du repertoire + Copie du fichier + Chiffrement du script + Remplacement du fichier + Preparation de la config de build + Lecture des ressources d\'ecran de demarrage + Configuration du manifest + Configuration du nom de package + Copie des assets vers + Traitement du code source + Application des ressources binaires + Copie des bibliotheques natives + Mise a jour de la config du projet + Etape de signature terminee + Ecriture de la config du projet + Compilation des ressources + Build termine + Ecriture de resources.arsc + Ecriture du manifest + Ecriture de l\'icone de l\'appli + Copie d\'asset + Preparation du dir des assets + Copie de bibliotheque + Preparation du keystore + APK non signe cree + Creation de l\'APK non signe + Utilisation du keystore + Re-signature de l\'APK + Ecriture de l\'APK signe + Signature terminee + Nettoyage termine + Nettoyage de l\'espace de travail + Resolution des bibliotheques natives du plugin + Copie des bibliotheques pour ABI + Plugin selectionne + Bibliotheques du plugin extraites + Assets du plugin extraits + Extraction d\'asset du plugin + Extraction du fichier so du plugin \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b1b770af..c55233ee 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -8,11 +8,11 @@ - 建築... - 洗浄... - パッケージング... + 建築 + 洗浄 + パッケージング apk builder の非互換バージョン.\n今すぐダウンロードしますか? - 準備中... + 準備中 デバイス SDK ファイルサイズ インストール済み @@ -535,7 +535,7 @@ 文書 AutoJs6 ログ ログ - AutoJs6 プラグインセンター + AutoJs6 プラグイン プラグイン AutoJs6 設定 設定 @@ -673,7 +673,7 @@ フォルダーを削除 行削除 この履歴を完全に削除しますか? - 削除中... + 削除中 説明 全て解除 宛先 @@ -1357,4 +1357,58 @@ プラグイン \"%1$s\" はプラグインセンターで許可されていません インストール済みアプリにプラグイン \"%1$s\" が見つかりません サポートされていないプラグイン接続方式です + %1$s: + タイトル + 内容 + APK をビルド中 + 生成された APK のパス + クリーン + 署名 + ビルド + 準備 + ワークスペースを準備中 + テンプレート APK を展開中 + 準備完了 + プロジェクトディレクトリをコピー中 + スクリプトファイルをコピー中 + ソース処理完了 + ディレクトリをコピー中 + ファイルをコピー中 + スクリプトを暗号化中 + ファイルを置換中 + ビルド設定を準備中 + スプラッシュ資源を読み込み中 + manifest を設定中 + パッケージ名を設定中 + assets をコピー中: + ソースを処理中 + バイナリ資源を適用中 + ネイティブライブラリをコピー中 + プロジェクト設定を更新中 + 署名ステージ完了 + プロジェクト設定を書き込み中 + 資源をビルド中 + ビルド完了 + resources.arsc を書き込み中 + manifest を書き込み中 + アプリアイコンを書き込み中 + asset をコピー中 + assets ディレクトリを準備中 + ライブラリをコピー中 + keystore を準備中 + 未署名 APK を作成しました + 未署名 APK を作成中 + keystore を使用中 + APK を再署名中 + 署名済み APK を書き込み中 + 署名完了 + クリーン完了 + ワークスペースをクリーン中 + プラグインのネイティブライブラリを解決中 + ABI 用ライブラリをコピー中 + 選択したプラグイン + プラグインのライブラリを抽出しました + プラグインの資源を抽出しました + プラグイン資源を抽出中 + プラグインの so を抽出中 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 40fbb402..4e2eac2f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -9,11 +9,11 @@ - 건물... - 청소... - 포장... + 건물 + 청소 + 포장 \"APK Builder\" 의 호환되지 않는 버전.\n지금 다운로드 하시겠습니까? - 준비... + 준비 디바이스 SDK 파일 크기 설치됨 @@ -536,7 +536,7 @@ 문서 AutoJs6 통나무 통나무 - AutoJs6 플러그인 센터 + AutoJs6 플러그인 플러그인 AutoJs6 설정 설정 @@ -674,7 +674,7 @@ 폴더 삭제 라인 삭제 이 버전을 영구 삭제할까요? - 삭제 중... + 삭제 중 설명 전체 해제 대상 @@ -1086,7 +1086,7 @@ 창을 닫으려면 \"뒤로\" 또는 \"볼륨 감소\" 버튼을 누르세요 시사 프로세스 로그 - 처리 + 처리 중 프로젝트 프로젝트 위치 프로젝트 미디어 액세스 @@ -1358,4 +1358,58 @@ 플러그인 \"%1$s\" 이 (가) 플러그인 센터에서 권한이 부여되지 않았습니다 설치된 앱에서 플러그인 \"%1$s\" 을 (를) 찾을 수 없습니다 지원되지 않는 플러그인 연결 방식입니다 + %1$s: + 제목 + 내용 + APK 빌드 중 + 생성된 APK 경로 + 정리 + 서명 + 빌드 + 준비 + 작업 공간 준비 중 + 템플릿 APK 추출 중 + 준비 완료 + 프로젝트 디렉터리 복사 중 + 스크립트 파일 복사 중 + 소스 처리 완료 + 디렉터리 복사 중 + 파일 복사 중 + 스크립트 암호화 중 + 파일 교체 중 + 빌드 설정 준비 중 + 스플래시 리소스 읽는 중 + manifest 설정 중 + 패키지 이름 설정 중 + assets 복사 중: + 소스 처리 중 + 바이너리 리소스 적용 중 + 네이티브 라이브러리 복사 중 + 프로젝트 설정 업데이트 중 + 서명 단계 완료 + 프로젝트 설정 기록 중 + 리소스 빌드 중 + 빌드 완료 + resources.arsc 기록 중 + manifest 기록 중 + 앱 아이콘 기록 중 + asset 복사 중 + assets 디렉터리 준비 중 + 라이브러리 복사 중 + keystore 준비 중 + 서명되지 않은 APK가 생성됨 + 서명되지 않은 APK 생성 중 + keystore 사용 중 + APK 재서명 중 + 서명된 APK 기록 중 + 서명 완료 + 정리 완료 + 작업 공간 정리 중 + 플러그인 네이티브 라이브러리 확인 중 + ABI용 라이브러리 복사 중 + 선택된 플러그인 + 플러그인 라이브러리를 추출함 + 플러그인 assets를 추출함 + 플러그인 asset 추출 중 + 플러그인 so 파일 추출 중 \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b21073f7..61129d1f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,11 +7,11 @@ - Строительство... - Очистка... - Упаковка... + Строительство + Очистка + Упаковка Несовместимая версия \"apk builder\".\nЗагрузить ее сейчас? - Подготовка... + Подготовка SDK устройства Размер файла Установлено @@ -534,7 +534,7 @@ документы AutoJs6 Журнал Журнал - Центр плагинов AutoJs6 + AutoJs6 Плагины Плагины AutoJs6 Настройки Настройки @@ -672,7 +672,7 @@ Удалить папку Удалить строку Удалить эту ревизию навсегда? - Удаление... + Удаление Описание Снять выделение Назначение @@ -1356,4 +1356,58 @@ Плагин \"%1$s\" не авторизован в Центре плагинов Плагин \"%1$s\" не найден среди установленных приложений Неподдерживаемое подключение плагина + %1$s: + Заголовок + Содержимое + Сборка APK + Путь к собранному APK + Очистить + Подписать + Собрать + Подготовить + Подготовка рабочей области + Извлечение шаблонного APK + Подготовка завершена + Копирование каталога проекта + Копирование файла скрипта + Обработка исходников завершена + Копирование каталога + Копирование файла + Шифрование скрипта + Замена файла + Подготовка конфигурации сборки + Чтение ресурсов заставки + Настройка manifest + Настройка имени пакета + Копирование assets в + Обработка исходников + Применение бинарных ресурсов + Копирование нативных библиотек + Обновление конфигурации проекта + Этап подписи завершен + Запись конфигурации проекта + Сборка ресурсов + Сборка завершена + Запись resources.arsc + Запись manifest + Запись значка приложения + Копирование asset + Подготовка каталога assets + Копирование библиотеки + Подготовка keystore + Неподписанный APK создан + Создание неподписанного APK + Использование keystore + Повторная подпись APK + Запись подписанного APK + Подпись завершена + Очистка завершена + Очистка рабочей области + Разбор нативных библиотек плагина + Копирование библиотек для ABI + Выбранный плагин + Библиотеки плагина извлечены + Assets плагина извлечены + Извлечение asset плагина + Извлечение файла so плагина \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index e5b0c0ad..cd35c113 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -4,11 +4,11 @@ - 構建中... - 清理臨時文件... - 打包中... + 構建中 + 清理臨時文件 + 打包中 打包插件需更新\n是否下載 - 準備文件... + 準備文件 設備 SDK 文件大小 已安裝 @@ -530,7 +530,7 @@ 文檔 AutoJs6 日誌 日誌 - AutoJs6 插件中心 + AutoJs6 插件 插件 AutoJs6 設置 設置 @@ -668,7 +668,7 @@ 刪除文件夾 刪除行 是否永久刪除此版本記錄 - 正在刪除... + 正在刪除 描述 取消全選 目標 @@ -1080,7 +1080,7 @@ 如需關閉窗口, 可按 \"返回鍵\" 或 \"音量減鍵\" 預覽 流程日誌 - 處理中 + 正在處理 項目 項目位置 投影媒體權限 @@ -1352,4 +1352,58 @@ 插件 \"%1$s\" 未在插件中心獲得授權 已安裝應用中未找到插件 \"%1$s\" 不支持的插件連接方式 + %1$s: + 標題 + 內容 + 正在打包應用 + 打包應用文件路徑 + 清理 + 簽名 + 構建 + 準備 + 正在準備工作區 + 正在提取模板 APK + 準備完成 + 正在複製項目目錄 + 正在複製腳本文件 + 源代碼處理完成 + 正在複製目錄 + 正在複製文件 + 正在加密腳本 + 正在替換文件 + 正在準備構建配置 + 正在讀取啓動頁資源 + 正在配置清單文件 + 正在配置包名 + 正在複製資源到 + 正在處理源代碼 + 正在應用二進制資源 + 正在複製原生庫 + 正在更新項目配置 + 簽名階段完成 + 正在寫入項目配置 + 正在構建資源 + 構建完成 + 正在寫入 resources.arsc + 正在寫入清單文件 + 正在寫入應用圖標 + 正在複製資源 + 正在準備資源目錄 + 正在複製庫文件 + 正在準備密鑰庫 + 未簽名 APK 已創建 + 正在創建未簽名 APK + 正在使用密鑰庫 + 正在重新簽名 APK + 正在寫入已簽名 APK + 簽名完成 + 清理完成 + 正在清理工作區 + 正在解析插件原生庫 + 正在複製 ABI 庫文件 + 已選擇插件 + 已提取插件庫文件 + 已提取插件資源 + 正在提取插件資源 + 正在提取插件 so 文件 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca94520f..8a295570 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -4,11 +4,11 @@ - 構建中... - 清理臨時檔案... - 打包中... + 構建中 + 清理臨時檔案 + 打包中 打包外掛需更新\n是否下載 - 準備檔案... + 準備檔案 裝置 SDK 檔案大小 已安裝 @@ -530,7 +530,7 @@ 文件 AutoJs6 日誌 日誌 - AutoJs6 外掛中心 + AutoJs6 外掛 外掛 AutoJs6 設定 設定 @@ -668,7 +668,7 @@ 刪除資料夾 刪除行 是否永久刪除此版本記錄 - 正在刪除... + 正在刪除 描述 取消全選 目標 @@ -1080,7 +1080,7 @@ 如需關閉視窗, 可按 \"返回鍵\" 或 \"音量減鍵\" 預覽 流程日誌 - 處理中 + 正在處理 專案 專案位置 投影媒體許可權 @@ -1352,4 +1352,58 @@ 外掛 \"%1$s\" 未在外掛中心獲得授權 已安裝應用中未找到外掛 \"%1$s\" 不支援的外掛連線方式 + %1$s: + 標題 + 內容 + 正在打包應用 + 打包應用檔案路徑 + 清理 + 簽名 + 構建 + 準備 + 正在準備工作區 + 正在提取模板 APK + 準備完成 + 正在複製專案目錄 + 正在複製指令碼檔案 + 原始碼處理完成 + 正在複製目錄 + 正在複製檔案 + 正在加密指令碼 + 正在替換檔案 + 正在準備構建配置 + 正在讀取啟動頁資源 + 正在配置清單檔案 + 正在配置包名 + 正在複製資源到 + 正在處理原始碼 + 正在應用二進位制資源 + 正在複製原生庫 + 正在更新專案配置 + 簽名階段完成 + 正在寫入專案配置 + 正在構建資源 + 構建完成 + 正在寫入 resources.arsc + 正在寫入清單檔案 + 正在寫入應用圖示 + 正在複製資源 + 正在準備資源目錄 + 正在複製庫檔案 + 正在準備金鑰庫 + 未簽名 APK 已建立 + 正在建立未簽名 APK + 正在使用金鑰庫 + 正在重新簽名 APK + 正在寫入已簽名 APK + 簽名完成 + 清理完成 + 正在清理工作區 + 正在解析外掛原生庫 + 正在複製 ABI 庫檔案 + 已選擇外掛 + 已提取外掛庫檔案 + 已提取外掛資源 + 正在提取外掛資源 + 正在提取外掛 so 檔案 \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 0f49fa78..552d173e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -4,11 +4,11 @@ - 构建中... - 清理临时文件... - 打包中... + 构建中 + 清理临时文件 + 打包中 打包插件需更新\n是否下载 - 准备文件... + 准备文件 设备 SDK 文件大小 已安装 @@ -531,7 +531,7 @@ 文档 AutoJs6 日志 日志 - AutoJs6 插件中心 + AutoJs6 插件 插件 AutoJs6 设置 设置 @@ -669,7 +669,7 @@ 删除文件夹 删除行 是否永久删除此版本记录 - 正在删除... + 正在删除 描述 取消全选 目标 @@ -1081,7 +1081,7 @@ 如需关闭窗口, 可按 \"返回键\" 或 \"音量减键\" 预览 流程日志 - 处理中 + 正在处理 项目 项目位置 投影媒体权限 @@ -1353,4 +1353,58 @@ 插件 \"%1$s\" 未在插件中心获得授权 已安装应用中未找到插件 \"%1$s\" 不支持的插件连接方式 + %1$s: + 标题 + 内容 + 正在打包应用 + 打包应用文件路径 + 清理 + 签名 + 构建 + 准备 + 正在准备工作区 + 正在提取模板 APK + 准备完成 + 正在复制项目目录 + 正在复制脚本文件 + 源代码处理完成 + 正在复制目录 + 正在复制文件 + 正在加密脚本 + 正在替换文件 + 正在准备构建配置 + 正在读取启动页资源 + 正在配置清单文件 + 正在配置包名 + 正在复制资源到 + 正在处理源代码 + 正在应用二进制资源 + 正在复制原生库 + 正在更新项目配置 + 签名阶段完成 + 正在写入项目配置 + 正在构建资源 + 构建完成 + 正在写入 resources.arsc + 正在写入清单文件 + 正在写入应用图标 + 正在复制资源 + 正在准备资源目录 + 正在复制库文件 + 正在准备密钥库 + 未签名 APK 已创建 + 正在创建未签名 APK + 正在使用密钥库 + 正在重新签名 APK + 正在写入已签名 APK + 签名完成 + 清理完成 + 正在清理工作区 + 正在解析插件原生库 + 正在复制 ABI 库文件 + 已选择插件 + 已提取插件库文件 + 已提取插件资源 + 正在提取插件资源 + 正在提取插件 so 文件 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index aaa113b6..20b3a763 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -69,5 +69,9 @@ 16sp 48dp 40dp + 24dp + 12dp + 16sp + 12dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fe848c3..7f949f53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -277,11 +277,11 @@ - Building... - Cleaning... - Packaging... + Building + Cleaning + Packaging Incompatible version of \"apk builder\".\nDownload it now? - Preparing... + Preparing Device SDK File size Installed @@ -807,7 +807,7 @@ Docs AutoJs6 Log Log - AutoJs6 Plugin Center + AutoJs6 Plugins Plugins AutoJs6 Settings Settings @@ -945,7 +945,7 @@ Delete folder Delete line Delete this revision permanently? - Deleting... + Deleting Description Deselect all Destination @@ -1048,7 +1048,7 @@ File name File name: %1$s File does not exist - File Path + File path File \"%1$s\" is not an executable script Filename cannot be empty Filename cannot contain the following characters: \\ / : * ? " < > | @@ -1629,4 +1629,58 @@ Plugin \"%1$s\" is not authorized in Plugin Center Plugin \"%1$s\" not found in installed apps Unsupported plugin connection + %1$s: + Title + Content + Building APK + Built APK file path + Clean + Sign + Build + Prepare + Preparing workspace + Extracting template APK + Prepare completed + Copying project directory + Copying script file + Source processing completed + Copying directory + Copying file + Encrypting script + Replacing file + Preparing build config + Reading splash resources + Configuring manifest + Configuring package name + Copying assets to + Processing source + Applying binary resources + Copying native libraries + Updating project config + Sign stage completed + Writing project config + Building resources + Build completed + Writing resources.arsc + Writing manifest + Writing app icon + Copying asset + Preparing assets dir + Copying library + Preparing keystore + Unsigned APK created + Creating unsigned APK + Using keystore + Re-signing APK + Writing signed APK + Sign completed + Clean completed + Cleaning workspace + Resolving plugin native libraries + Copying libraries for ABI + Selected plugin + Extracted plugin libraries + Extracted plugin assets + Extracting plugin asset + Extracting plugin so diff --git a/version.properties b/version.properties index b772f28b..35a85917 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sun Mar 01 17:58:13 CST 2026 -BUILD_TIME=1772359093087 +#Sat Mar 07 12:33:49 CST 2026 +BUILD_TIME=1772858029128 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3761 -VERSION_NAME=6.7.0 Alpha22 +VERSION_BUILD=3778 +VERSION_NAME=6.7.0 Alpha23 VSCODE_EXT_REQUIRED_VERSION=1.0.13