From 9757e3690b5eb7b7768f1004fab0ed09f13ebe42 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sun, 15 Feb 2026 17:09:30 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha20=20-=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E8=87=AA=E5=8A=A8=E6=B8=85?= =?UTF-8?q?=E7=90=86=E6=9C=BA=E5=88=B6;=20=E5=A2=9E=E5=BC=BA=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BC=96=E8=BE=91=E5=99=A8=E5=90=8E=E5=8F=B0=E8=8D=89?= =?UTF-8?q?=E7=A8=BF=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 3 +- app/src/main/java/org/autojs/autojs/App.kt | 3 + .../main/java/org/autojs/autojs/pio/PFiles.kt | 37 +++++++ .../storage/file/StableDraftFileHelper.kt | 56 ++++++++++ .../autojs/storage/file/TmpScriptFiles.java | 6 + .../file/TmpScriptFilesCleanupScheduler.kt | 62 +++++++++++ .../file/TmpScriptFilesCleanupWorker.kt | 28 +++++ .../org/autojs/autojs/ui/edit/EditActivity.kt | 103 ++++++++++-------- .../org/autojs/autojs/ui/edit/EditorView.kt | 9 +- .../autojs/ui/edit/editor/CodeEditText.kt | 72 +++++++++++- app/src/main/res/layout/editor_view.xml | 3 +- .../res/layout/floating_manual_dialog.xml | 28 ++--- version.properties | 6 +- 13 files changed, 347 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/storage/file/StableDraftFileHelper.kt create mode 100644 app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupScheduler.kt create mode 100644 app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupWorker.kt diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 3ba85c17..4467e3ac 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,7 +1,7 @@ { "$data": { "v6.7.0": { - "released_date": "2026/02/12", + "released_date": "2026/02/15", "feature": [ "插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)", "版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)", @@ -103,6 +103,7 @@ "代码编辑器在只读模式下点击标题区域及部分菜单项导致应用崩溃的问题", "代码编辑器加载大文件可能导致应用崩溃的问题 (试修)", "代码编辑器自动打开新建文件时功能按钮状态初始化异常", + "代码编辑器编辑文件时置于后台可能导致应用崩溃的问题", "Android 15+ 代码编辑器点击 fx 按钮无法显示模块函数快捷面板的问题 (试修)", "ErrorDialogActivity 可能无法正常启动或短时间自动消失的问题 _[`issue #479`](http://issues.autojs6.com/479)_ _[`issue #471`](http://issues.autojs6.com/471)_ _[`issue #414`](http://issues.autojs6.com/414)_ _[`issue #340`](http://issues.autojs6.com/340#issuecomment-2973485826)_", "Canvas 构造函数可接受的参数类型错误 _[`issue #402`](http://issues.autojs6.com/402)_", diff --git a/app/src/main/java/org/autojs/autojs/App.kt b/app/src/main/java/org/autojs/autojs/App.kt index e1b10751..7bed78e2 100644 --- a/app/src/main/java/org/autojs/autojs/App.kt +++ b/app/src/main/java/org/autojs/autojs/App.kt @@ -25,6 +25,7 @@ import org.autojs.autojs.event.GlobalKeyObserver import org.autojs.autojs.external.receiver.DynamicBroadcastReceivers import org.autojs.autojs.ipc.InAppEventBus import org.autojs.autojs.leakcanary.LeakCanarySetup +import org.autojs.autojs.storage.file.TmpScriptFilesCleanupScheduler import org.autojs.autojs.storage.history.HistoryCleanupScheduler import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.timing.TimedTaskManager @@ -76,6 +77,8 @@ class App : MultiDexApplication() { HistoryCleanupScheduler.scheduleStartupCleanup(this) HistoryCleanupScheduler.schedulePeriodicCleanup(this) + TmpScriptFilesCleanupScheduler.scheduleStartupCleanup(this) + TmpScriptFilesCleanupScheduler.schedulePeriodicCleanup(this) } } } diff --git a/app/src/main/java/org/autojs/autojs/pio/PFiles.kt b/app/src/main/java/org/autojs/autojs/pio/PFiles.kt index 61fb3cba..a480f841 100644 --- a/app/src/main/java/org/autojs/autojs/pio/PFiles.kt +++ b/app/src/main/java/org/autojs/autojs/pio/PFiles.kt @@ -368,6 +368,43 @@ object PFiles { } } + @JvmStatic + @JvmOverloads + fun deleteRecursivelyOlderThan(file: File, maxAgeMs: Long, now: Long = System.currentTimeMillis()): Boolean { + if (!file.exists()) return true + + var ok = true + + if (file.isDirectory) { + val dirLastModified = file.lastModified() + val children = file.listFiles() ?: return false + + for (child in children) { + val childResult = deleteRecursivelyOlderThan(child, maxAgeMs, now) + if (!childResult) { + ok = false + } + } + + val remaining = file.listFiles() + val isEmpty = (remaining != null && remaining.isEmpty()) + + if (isEmpty) { + val age = now - dirLastModified + if (age >= maxAgeMs) { + ok = runCatching { file.delete() }.getOrDefault(false) && ok + } + } + return ok + } + + val age = now - file.lastModified() + if (age < maxAgeMs) return true + + val deleted = runCatching { file.delete() }.getOrDefault(false) + return deleted && ok + } + @JvmStatic fun deleteRecursively(file: File): Boolean { if (file.isDirectory()) { diff --git a/app/src/main/java/org/autojs/autojs/storage/file/StableDraftFileHelper.kt b/app/src/main/java/org/autojs/autojs/storage/file/StableDraftFileHelper.kt new file mode 100644 index 00000000..e39e4614 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/file/StableDraftFileHelper.kt @@ -0,0 +1,56 @@ +package org.autojs.autojs.storage.file + +import android.content.Context +import org.autojs.autojs.pio.PFiles +import java.io.File +import java.security.MessageDigest +import java.util.Locale + +/** + * Created by SuperMonster003 on Feb 15, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 15, 2026. + */ +class StableDraftFileHelper(private val context: Context, private val keyPath: String? = null) { + + fun saveDraft(text: String): File? = + runCatching { + val target = stableDraftFileForKeyPathOrNull(keyPath) ?: return@runCatching null + + // Atomic write: write to .part then rename. + // zh-CN: 原子写入: 先写入 .part 再重命名. + val part = File(target.parentFile, target.name + ".part") + + PFiles.write(part, text) + + if (!part.renameTo(target)) { + // Fallback: try best-effort copy/replace. + // zh-CN: 后备方案: 尽力 copy/replace. + PFiles.write(target, text) + runCatching { part.delete() } + } + + target + }.onFailure { it.printStackTrace() }.getOrNull() + + fun deleteDraft() { + stableDraftFileForKeyPathOrNull(keyPath)?.let { f -> + // Best-effort cleanup. + // zh-CN: 尽力清理. + runCatching { f.delete() } + runCatching { File(f.parentFile, f.name + ".part").delete() } + } + } + + private fun stableDraftFileForKeyPathOrNull(keyPath: String?): File? { + if (keyPath.isNullOrBlank()) return null + + // Stable name: sha256(path).js. + // zh-CN: 稳定文件名: sha256(path).js. + val digest = MessageDigest.getInstance("SHA-256") + .digest(keyPath.toByteArray(Charsets.UTF_8)) + .joinToString("") { "%02x".format(Locale.US, it) } + + val dir = File(context.cacheDir, "editor-drafts").apply { mkdirs() } + return File(dir, "draft-$digest.js") + } +} diff --git a/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFiles.java b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFiles.java index dcf76aa6..f8675407 100644 --- a/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFiles.java +++ b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFiles.java @@ -10,6 +10,7 @@ import static org.autojs.autojs.util.FileUtils.TYPE.JAVASCRIPT; /** * Created by Stardust on Oct 21, 2017. + * Modified by SuperMonster003 as of Feb 15, 2026. */ public class TmpScriptFiles { @@ -20,6 +21,11 @@ public class TmpScriptFiles { return tmp; } + public static void clearTmpDir(Context context, long maxAgeMs) { + File dir = getTmpDir(context); + PFiles.deleteRecursivelyOlderThan(dir, maxAgeMs); + } + public static void clearTmpDir(Context context) { File dir = getTmpDir(context); PFiles.deleteRecursively(dir); diff --git a/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupScheduler.kt b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupScheduler.kt new file mode 100644 index 00000000..41b74929 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupScheduler.kt @@ -0,0 +1,62 @@ +package org.autojs.autojs.storage.file + +import android.content.Context +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit + +/** + * Created by SuperMonster003 on Feb 8, 2026. + */ +object TmpScriptFilesCleanupScheduler { + + private const val UNIQUE_WORK_NAME = "tmp_script_files_cleanup_periodic" + + fun scheduleStartupCleanup(context: Context) { + val appContext = context.applicationContext + + runCatching { + val req = OneTimeWorkRequestBuilder() + // Delay a bit to reduce cold-start I/O pressure. + // zh-CN: 适当延迟以降低冷启动 I/O 压力. + .setInitialDelay(10L, TimeUnit.SECONDS) + .build() + + WorkManager.getInstance(appContext) + .enqueueUniqueWork( + "startup-tmp-script-files-cleanup", + ExistingWorkPolicy.KEEP, + req, + ) + } + } + + /** + * Schedule periodic cleanup (once per day). + * zh-CN: 调度周期性清理 (每天一次). + */ + fun schedulePeriodicCleanup(context: Context) { + val appContext = context.applicationContext + + // No network is required; keep it light and battery-friendly. + // zh-CN: 不需要网络; 尽量轻量/省电. + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.NOT_REQUIRED) + .build() + + val request = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(appContext).enqueueUniquePeriodicWork( + UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + request, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupWorker.kt b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupWorker.kt new file mode 100644 index 00000000..bc8b8081 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/file/TmpScriptFilesCleanupWorker.kt @@ -0,0 +1,28 @@ +package org.autojs.autojs.storage.file + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters + +/** + * Created by SuperMonster003 on Feb 15, 2026. + */ +class TmpScriptFilesCleanupWorker( + appContext: Context, + params: WorkerParameters, +) : CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + return runCatching { + val maxAgeMs = TMP_FILES_CLEANUP_THRESHOLD + TmpScriptFiles.clearTmpDir(applicationContext, maxAgeMs) + }.fold( + onSuccess = { Result.success() }, + onFailure = { Result.retry() }, + ) + } + + companion object { + private const val TMP_FILES_CLEANUP_THRESHOLD: Long = 7L * 24L * 60L * 60L * 1000L + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt index 2c423cd8..9d64787b 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt @@ -31,7 +31,7 @@ import org.autojs.autojs.core.permission.OnRequestPermissionsResultCallback import org.autojs.autojs.core.permission.PermissionRequestProxyActivity import org.autojs.autojs.core.permission.RequestPermissionCallbacks import org.autojs.autojs.pio.PFiles -import org.autojs.autojs.storage.file.TmpScriptFiles +import org.autojs.autojs.storage.file.StableDraftFileHelper import org.autojs.autojs.theme.widget.ThemeColorToolbar import org.autojs.autojs.ui.BaseActivity import org.autojs.autojs.ui.error.ErrorDialogActivity @@ -47,12 +47,11 @@ import org.autojs.autojs.util.ViewUtils.titleView import org.autojs.autojs6.R import org.autojs.autojs6.databinding.ActivityEditBinding import java.io.File -import java.io.IOException /** * Created by Stardust on Jan 29, 2017. - * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 12, 2026. - * Modified by SuperMonster003 as of Feb 12, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 15, 2026. + * Modified by SuperMonster003 as of Feb 15, 2026. */ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyActivity { @@ -89,34 +88,49 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc private val mRequestPermissionCallbacks = RequestPermissionCallbacks() private var mNewTask = false + private lateinit var draftFileHelper: StableDraftFileHelper + @SuppressLint("CheckResult") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mReadOnly = intent.getBooleanExtra(EditorView.EXTRA_READ_ONLY, false) + val binding = ActivityEditBinding.inflate(layoutInflater).also { + setContentView(it.root) + } + val readOnly = intent.getBooleanExtra(EditorView.EXTRA_READ_ONLY, false).also { + mReadOnly = it + } + val toolbar = findViewById(R.id.toolbar).also { + mToolbar = it + } + val editorView = binding.editorView.also { + mEditorView = it + } + EditorMenu(editorView, readOnly).also { + mEditorMenu = it + } + StableDraftFileHelper(this, editorView.uri?.path).also { + draftFileHelper = it + } + (intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0).also { + mNewTask = it + } - val binding = ActivityEditBinding.inflate(layoutInflater).also { setContentView(it.root) } - mToolbar = findViewById(R.id.toolbar).apply { - setTitleTextAppearance(this@EditActivity, R.style.TextAppearanceEditorTitle) - setOnTitleViewClickListener { - val path = mEditorView.uri?.path - if (path != null) { - EditableFileInfoDialogManager.showEditableFileInfoDialog(this@EditActivity, File(path)) { - mEditorView.editor.text - } + toolbar.setTitleTextAppearance(this, R.style.TextAppearanceEditorTitle) + toolbar.setOnTitleViewClickListener { + val path = mEditorView.uri?.path + if (path != null) { + EditableFileInfoDialogManager.showEditableFileInfoDialog(this, File(path)) { + mEditorView.editor.text } } } - mEditorView = binding.editorView.apply { - handleIntent(intent) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(Observers.emptyConsumer()) { ex: Throwable -> onLoadFileError(ex.message) } - } - mEditorMenu = EditorMenu(mEditorView, mReadOnly) - mNewTask = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0 - setUpToolbar() + editorView.handleIntent(intent) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(Observers.emptyConsumer()) { ex: Throwable -> onLoadFileError(ex.message) } + setToolbarAsBack(editorView.name) onBackPressedDispatcher.addCallback(this, mOnBackPressedCallback) } @@ -131,10 +145,6 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc .show() } - private fun setUpToolbar() { - setToolbarAsBack(mEditorView.name) - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_editor, menu) mToolbar?.let { toolbar -> @@ -350,6 +360,10 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc .save() .observeOn(AndroidSchedulers.mainThread()) .subscribe({ + // Save succeeded: remove draft for this file. + // zh-CN: 保存成功: 删除该文件对应草稿. + draftFileHelper.deleteDraft() + runCatching { d.dismiss() } finishAndRemoveFromRecents() }, { e: Throwable -> @@ -389,33 +403,32 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc } override fun onSaveInstanceState(outState: Bundle) { - if (!mEditorView.isTextChanged) { + // Save draft when UI indicates "needs save". + // zh-CN: 当 UI 表示 "需要保存" 时保存草稿. + if (!mEditorView.saveStickyDirty) { + super.onSaveInstanceState(outState) return } + val text = mEditorView.editor.text - if (text.length < 256 * 1024) { - outState.putString("text", text) - } else { - val tmp = saveToTmpFile(text) - if (tmp != null) { - outState.putString("path", tmp.path) + when { + text.length < 256 * 1024 -> { + Log.d(LOG_TAG, "saveDraftText, length: ${text.length}") + outState.putString("text", text) + } + else -> { + Log.d(LOG_TAG, "saveDraftFile, length: ${text.length}") + + // Use stable file key (real script path) to avoid accumulating tmp files. + // zh-CN: 使用稳定 key (脚本真实 path) 避免累计 tmp 文件. + draftFileHelper.saveDraft(text)?.let { tmp -> + outState.putString("path", tmp.path) + } } } super.onSaveInstanceState(outState) } - @SuppressLint("CheckResult") - private fun saveToTmpFile(text: String): File? = try { - TmpScriptFiles.create(this).also { tmp -> - Observable.just(text) - .observeOn(Schedulers.io()) - .subscribe { t: String? -> PFiles.write(tmp, t) } - } - } catch (e: IOException) { - e.printStackTrace() - null - } - override fun onResume() { super.onResume() runCatching { mEditorView.refreshSymbolsBar() } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt index 8c537ce0..d9a36b04 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt @@ -58,6 +58,7 @@ import org.autojs.autojs.model.script.Scripts.openByOtherApps import org.autojs.autojs.model.script.Scripts.runWithBroadcastSender import org.autojs.autojs.pio.PFiles.getNameWithoutExtension import org.autojs.autojs.pio.PFiles.write +import org.autojs.autojs.storage.file.StableDraftFileHelper import org.autojs.autojs.storage.file.TmpScriptFiles import org.autojs.autojs.storage.history.HistoryPrefs import org.autojs.autojs.storage.history.HistoryRepository @@ -119,8 +120,8 @@ import java.util.regex.Pattern /** * Created by Stardust on Sep 28, 2017. * Transformed by SuperMonster003 on May 1, 2023. - * Modified by SuperMonster003 as of Feb 3, 2026. * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 12, 2026. + * Modified by SuperMonster003 as of Feb 15, 2026. */ @SuppressLint("CheckResult") class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFragment.OnMenuItemClickListener { @@ -1055,6 +1056,8 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // zh-CN: 因刚刚建立了新的基线, 重置 sticky 脏标记. saveStickyDirty = false mHadDirectEditSinceSave = false + + mEditorLoading = false syncPrimaryMenuState() // Refresh highlight for restored text if size allows. @@ -1556,6 +1559,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag mHadDirectEditSinceSave = false setMenuItemStatus(R.id.save, false) + + val keyPath = uri?.path + val draftFileHelper = StableDraftFileHelper(context, keyPath) + draftFileHelper.deleteDraft() } /** diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt index 242f5331..88282b95 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.kt @@ -35,6 +35,7 @@ import android.util.AttributeSet import android.view.Gravity import android.view.KeyEvent import android.view.MotionEvent +import android.view.accessibility.AccessibilityEvent import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnectionWrapper @@ -72,7 +73,11 @@ class CodeEditText : AppCompatEditText { // Fixed gutter digits used during loading to avoid frequent requestLayout(). // zh-CN: 加载期间使用固定 gutter 位数, 避免频繁 requestLayout(). @Volatile - private var mLoadingGutterDigits: Int = 1 + private var mLoadingGutterDigits: Int = 3 + + // Accessibility payload guardrail threshold. + // zh-CN: 无障碍事件负载护栏阈值. + private val mA11yLargeTextThresholdChars: Int = 64 * 1024 private var mTheme: Theme = Theme.getDefault(context) private val mLineHighlightPaint = Paint().apply { style = Paint.Style.FILL } @@ -136,6 +141,10 @@ class CodeEditText : AppCompatEditText { // zh-CN: 默认确保可以进行文本选择. setTextIsSelectable(true) isLongClickable = true + + // Update accessibility importance at init. + // zh-CN: 初始化时更新无障碍重要性配置. + updateAccessibilityImportanceIfNeeded() } // Toggle loading state. @@ -544,16 +553,50 @@ class CodeEditText : AppCompatEditText { return } - // 调用父类的 onSelectionChanged 时会发送一个 AccessibilityEvent, 当文本过大时造成异常 - // super.onSelectionChanged(selStart, selEnd); - // 父类构造函数会调用 onSelectionChanged, 此时 mCursorChangeCallbacks 还没有初始化 - super.onSelectionChanged(selStart, selEnd) + // Update accessibility importance when selection changes (input usually changes selection). + // zh-CN: selection 变化时更新无障碍重要性 (输入通常会改变 selection). + updateAccessibilityImportanceIfNeeded() + + // Avoid sending large accessibility events for huge text on every keystroke. + // zh-CN: 避免在超大文本下每次按键都发送巨大的无障碍事件. + if (shouldSuppressAccessibilityForLargeText()) { + // Do NOT call super.onSelectionChanged() here because it may dispatch + // TYPE_VIEW_TEXT_SELECTION_CHANGED with huge payload and cause Binder TTLE. + // zh-CN: + // 此处不要调用 super.onSelectionChanged(), + // 因其可能派发携带巨大负载的 TYPE_VIEW_TEXT_SELECTION_CHANGED, + // 从而导致 Binder TTLE. + } else { + // 调用父类的 onSelectionChanged 时会发送一个 AccessibilityEvent, 当文本过大时造成异常 + // super.onSelectionChanged(selStart, selEnd); + // 父类构造函数会调用 onSelectionChanged, 此时 mCursorChangeCallbacks 还没有初始化 + super.onSelectionChanged(selStart, selEnd) + } + mCursorChangeCallbacks?.let { it.takeUnless { it.isEmpty() } } ?: return if (selStart != selEnd) return callCursorChangeCallback(text, selStart) matchesBracket(text, selStart) } + override fun sendAccessibilityEventUnchecked(event: AccessibilityEvent) { + // Guardrail: drop or shrink accessibility payload when text is huge. + // zh-CN: 护栏: 文本很大时丢弃或瘦身无障碍事件负载. + if (shouldSuppressAccessibilityForLargeText()) { + // Clear potentially huge text payload to avoid Binder transaction overflow. + // zh-CN: 清空可能非常大的 text 负载, 避免 Binder 事务溢出. + runCatching { event.text.clear() } + runCatching { event.contentDescription = null } + + // Also drop selection-changed events entirely in large-text mode. + // zh-CN: 大文本模式下直接丢弃 selection-changed 事件. + if (event.eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) { + return + } + } + super.sendAccessibilityEventUnchecked(event) + } + private fun matchesBracket(text: CharSequence?, cursor: Int) { if (checkBracketMatchingAt(text, cursor)) return if (checkBracketMatchingAt(text, cursor - 1)) return @@ -713,6 +756,25 @@ class CodeEditText : AppCompatEditText { invalidate() } + // Whether accessibility should be suppressed for current content size. + // zh-CN: 是否需要针对当前内容大小抑制无障碍事件. + private fun shouldSuppressAccessibilityForLargeText(): Boolean { + val len = text?.length ?: 0 + return len >= mA11yLargeTextThresholdChars + } + + // Apply accessibility importance based on current text length. + // zh-CN: 根据当前文本长度应用无障碍重要性配置. + private fun updateAccessibilityImportanceIfNeeded() { + val target = when (shouldSuppressAccessibilityForLargeText()) { + true -> IMPORTANT_FOR_ACCESSIBILITY_NO + else -> IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + if (importantForAccessibility != target) { + importantForAccessibility = target + } + } + companion object { const val TAG = "CodeEditText" diff --git a/app/src/main/res/layout/editor_view.xml b/app/src/main/res/layout/editor_view.xml index 42b610db..d29d2a45 100644 --- a/app/src/main/res/layout/editor_view.xml +++ b/app/src/main/res/layout/editor_view.xml @@ -142,7 +142,8 @@ android:id="@+id/docs" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_gravity="start" /> + android:layout_gravity="start" + android:saveEnabled="false" /> diff --git a/app/src/main/res/layout/floating_manual_dialog.xml b/app/src/main/res/layout/floating_manual_dialog.xml index a930f46f..b77b5ad6 100644 --- a/app/src/main/res/layout/floating_manual_dialog.xml +++ b/app/src/main/res/layout/floating_manual_dialog.xml @@ -1,11 +1,12 @@ - + + tools:text="标题" /> + app:tint="#3c3c3c" /> + app:tint="#3c3c3c" /> + app:tint="#3c3c3c" /> + android:background="#cccccc" /> + android:layout_height="match_parent" + android:saveEnabled="false" /> \ No newline at end of file diff --git a/version.properties b/version.properties index 3e8a327f..88c1c881 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sat Feb 14 14:01:58 CST 2026 -BUILD_TIME=1771048918130 +#Sun Feb 15 16:19:31 CST 2026 +BUILD_TIME=1771143571726 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=3742 +VERSION_BUILD=3746 VERSION_NAME=6.7.0 Alpha20 VSCODE_EXT_REQUIRED_VERSION=1.0.13