6.7.0 - Alpha20 - 增加临时文件自动清理机制; 增强代码编辑器后台草稿稳定性
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<TmpScriptFilesCleanupWorker>()
|
||||
// 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<TmpScriptFilesCleanupWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(appContext).enqueueUniquePeriodicWork(
|
||||
UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
request,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<ThemeColorToolbar>(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<ThemeColorToolbar>(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() }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardBackgroundColor="@android:color/white"
|
||||
app:cardCornerRadius="6dp">
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardBackgroundColor="@android:color/white"
|
||||
app:cardCornerRadius="6dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -26,7 +27,7 @@
|
||||
android:paddingStart="20dp"
|
||||
android:textColor="#2a2a2a"
|
||||
android:textSize="16sp"
|
||||
tools:text="标题"/>
|
||||
tools:text="标题" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_to_left"
|
||||
@@ -35,7 +36,7 @@
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_ali_pin_to_left"
|
||||
app:tint="#3c3c3c" />
|
||||
app:tint="#3c3c3c" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fullscreen"
|
||||
@@ -44,7 +45,7 @@
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_ali_fullscreen"
|
||||
app:tint="#3c3c3c" />
|
||||
app:tint="#3c3c3c" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/close"
|
||||
@@ -54,18 +55,19 @@
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_ali_close"
|
||||
app:tint="#3c3c3c" />
|
||||
app:tint="#3c3c3c" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1.2px"
|
||||
android:background="#cccccc"/>
|
||||
android:background="#cccccc" />
|
||||
|
||||
<org.autojs.autojs.ui.widget.EWebView
|
||||
android:id="@+id/eweb_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:saveEnabled="false" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
Reference in New Issue
Block a user