From 06f1484d84b330fc78e98c70b65bfa1926fa8e3c Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 3 Apr 2026 17:02:39 -0700 Subject: [PATCH] ADFA-3133 Bug fix for over-eager auto-save during onPause event --- app/src/main/AndroidManifest.xml | 2 +- .../editor/EditorHandlerActivity.kt | 45 ++++++++++++++----- .../fragments/RecyclerViewFragment.kt | 42 ++++++++++++++--- .../itsaky/androidide/ui/CodeEditorView.kt | 9 ++++ 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7d1b948d2..8dcc77970f 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,7 +93,7 @@ android:theme="@style/Theme.AndroidIDE" /> val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach val currentTimestamp = file.lastModified() - // If the file on disk is newer. if (currentTimestamp > lastKnownTimestamp) { val newContent = runCatching { file.readText() }.getOrNull() ?: return@forEach withContext(Dispatchers.Main) { - // If the editor for the new file exists AND has no unsaved changes... val editorView = getEditorForFile(file) ?: return@withContext if (editorView.isModified) return@withContext + val ideEditor = editorView.editor ?: return@withContext + if (ideEditor.canUndo() || ideEditor.canRedo()) { + return@withContext + } - editorView.editor?.setText(newContent) + ideEditor.setText(newContent) editorView.markAsSaved() updateTabs() } @@ -341,12 +349,19 @@ open class EditorHandlerActivity : prefs.getString(PREF_KEY_OPEN_FILES_CACHE, null) } ?: return@launch + if (editorViewModel.getOpenedFileCount() > 0) { + // Returning to an in-memory session (e.g. after onPause/onStop). Replaying the + // snapshot would be redundant and could interfere with dirty buffers and undo. + withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) } + return@launch + } + val cache = withContext(Dispatchers.Default) { Gson().fromJson(jsonCache, OpenedFilesCache::class.java) } onReadOpenedFilesCache(cache) - // Clear the preference so it's only loaded once on startup + // Clear the preference so it's only loaded once per cold restore withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) } } catch (err: Throwable) { log.error("Failed to reopen recently opened files", err) @@ -747,6 +762,11 @@ open class EditorHandlerActivity : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) + val safeContent = contentOrNull ?: return + for (i in 0 until safeContent.editorContainer.childCount) { + (safeContent.editorContainer.getChildAt(i) as? CodeEditorView)?.reapplyEditorDisplayPreferences() + } + getCurrentEditor()?.editor?.apply { doOnNextLayout { cursor?.let { c -> ensurePositionVisible(c.leftLine, c.leftColumn, true) } @@ -1069,17 +1089,20 @@ open class EditorHandlerActivity : nameBuilder.addPath(it, it.path) } - for (index in 0 until content.tabs.tabCount) { - val file = files.getOrNull(index) ?: continue + for (tabPos in 0 until content.tabs.tabCount) { + if (isPluginTab(tabPos)) continue + val fileIndex = getFileIndexForTabPosition(tabPos) + if (fileIndex < 0) continue + val file = files.getOrNull(fileIndex) ?: continue val count = dupliCount[file.name] ?: 0 - val isModified = getEditorAtIndex(index)?.isModified ?: false + val isModified = getEditorAtIndex(fileIndex)?.isModified ?: false var name = if (count > 1) nameBuilder.getShortPath(file) else file.name if (isModified) { name = "*$name" } - names[index] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon + names[tabPos] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon } withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/itsaky/androidide/fragments/RecyclerViewFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/RecyclerViewFragment.kt index ba03fd8f4f..e14260035f 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/RecyclerViewFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/RecyclerViewFragment.kt @@ -20,12 +20,15 @@ package com.itsaky.androidide.fragments import android.annotation.SuppressLint import android.os.Bundle import android.view.GestureDetector +import android.view.LayoutInflater import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.LayoutManager -import com.itsaky.androidide.databinding.FragmentRecyclerviewBinding +import androidx.viewbinding.ViewBinding +import com.itsaky.androidide.R import com.itsaky.androidide.idetooltips.TooltipManager /** @@ -34,7 +37,7 @@ import com.itsaky.androidide.idetooltips.TooltipManager * @author Akash Yadav */ abstract class RecyclerViewFragment> : - EmptyStateFragment(FragmentRecyclerviewBinding::inflate) { + EmptyStateFragment(FragmentRecyclerviewManualBinding::inflate) { protected abstract val fragmentTooltipTag: String? private var unsavedAdapter: A? = null @@ -86,7 +89,7 @@ abstract class RecyclerViewFragment> : * Sets up the recycler view in the fragment. */ protected open fun onSetupRecyclerView() { - binding.root.apply { + binding.list.apply { layoutManager = onCreateLayoutManager() adapter = unsavedAdapter ?: onCreateAdapter() } @@ -107,7 +110,7 @@ abstract class RecyclerViewFragment> : onSetupRecyclerView() - binding.root.addOnItemTouchListener(touchListener) + binding.list.addOnItemTouchListener(touchListener) unsavedAdapter = null @@ -123,7 +126,7 @@ abstract class RecyclerViewFragment> : * Set the adapter for the [RecyclerView]. */ fun setAdapter(adapter: A) { - _binding?.root?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter } + _binding?.list?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter } if (isAdded && view != null) { checkIsEmpty() } @@ -142,6 +145,33 @@ abstract class RecyclerViewFragment> : private fun checkIsEmpty() { if (!isAdded || isDetached) return - isEmpty = _binding?.root?.adapter?.itemCount == 0 + isEmpty = _binding?.list?.adapter?.itemCount == 0 + } +} + +/** + * Manual [ViewBinding] for [R.layout.fragment_recyclerview] so annotation processors (kapt) do not + * depend on generated `FragmentRecyclerviewBinding` during stub analysis. + * + * Public (not internal/file-private): [RecyclerViewFragment] is public and Kotlin forbids a public + * class from using a non-public type as a [EmptyStateFragment] type argument. + * + * [getRoot] returns [RecyclerView] (covariant override), matching generated view binding so + * subclasses can use `binding.root.adapter` and other [RecyclerView] APIs. + */ +class FragmentRecyclerviewManualBinding( + val list: RecyclerView, +) : ViewBinding { + override fun getRoot(): RecyclerView = list + + companion object { + fun inflate( + inflater: LayoutInflater, + parent: ViewGroup?, + attachToParent: Boolean, + ): FragmentRecyclerviewManualBinding { + val root = inflater.inflate(R.layout.fragment_recyclerview, parent, false) as RecyclerView + return FragmentRecyclerviewManualBinding(root) + } } } diff --git a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt index f33540f1ad..3a5add1edb 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt @@ -567,6 +567,15 @@ class CodeEditorView( onPinLineNumbersPrefChanged() } + /** + * Re-applies display-related preferences (font size, typeface, flags) after a configuration change + * such as system font scale, so the editor activity can handle `fontScale` without being recreated. + */ + fun reapplyEditorDisplayPreferences() { + if (_binding == null) return + configureEditorIfNeeded() + } + private fun onMagnifierPrefChanged() { binding.editor.getComponent(Magnifier::class.java).isEnabled = EditorPreferences.useMagnifier