Skip to content

Commit 06f1484

Browse files
ADFA-3133 Bug fix for over-eager auto-save during onPause event
1 parent c85bf67 commit 06f1484

4 files changed

Lines changed: 80 additions & 18 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
android:theme="@style/Theme.AndroidIDE" />
9494
<activity
9595
android:name=".activities.editor.EditorActivityKt"
96-
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
96+
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|fontScale|density|uiMode"
9797
android:launchMode="singleTask"
9898
android:windowSoftInputMode="adjustResize" />
9999
<activity

app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ open class EditorHandlerActivity :
232232
loadPluginTabs()
233233
}
234234

235+
/**
236+
* Persists which tabs are open (preferences only). Does **not** write project file buffers to disk;
237+
* saving is explicit or prompted (e.g. close project).
238+
*/
235239
override fun onPause() {
236240
super.onPause()
237241
// Record timestamps for all currently open files before saving the cache
@@ -246,7 +250,6 @@ open class EditorHandlerActivity :
246250
if (!isOpenedFilesSaved.get()) {
247251
saveOpenedFiles()
248252
saveOpenedPluginTabs()
249-
saveAllAsync(notify = false)
250253
}
251254
}
252255

@@ -271,26 +274,31 @@ open class EditorHandlerActivity :
271274
invalidateOptionsMenu()
272275
}
273276

277+
/**
278+
* Reloads disk content into an open editor only when the file changed on disk since the last
279+
* [onPause] snapshot **and** the in-memory buffer is still clean ([CodeEditorView.isModified] is
280+
* false). Never replaces buffers with unsaved edits or touches undo history for dirty files.
281+
*/
274282
private fun checkForExternalFileChanges() {
275-
// Get the list of files currently managed by the ViewModel
276283
val openFiles = editorViewModel.getOpenedFiles()
277284
if (openFiles.isEmpty() || fileTimestamps.isEmpty()) return
278285

279286
lifecycleScope.launch(Dispatchers.IO) {
280-
// Check each open file
281287
openFiles.forEach { file ->
282288
val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach
283289
val currentTimestamp = file.lastModified()
284290

285-
// If the file on disk is newer.
286291
if (currentTimestamp > lastKnownTimestamp) {
287292
val newContent = runCatching { file.readText() }.getOrNull() ?: return@forEach
288293
withContext(Dispatchers.Main) {
289-
// If the editor for the new file exists AND has no unsaved changes...
290294
val editorView = getEditorForFile(file) ?: return@withContext
291295
if (editorView.isModified) return@withContext
296+
val ideEditor = editorView.editor ?: return@withContext
297+
if (ideEditor.canUndo() || ideEditor.canRedo()) {
298+
return@withContext
299+
}
292300

293-
editorView.editor?.setText(newContent)
301+
ideEditor.setText(newContent)
294302
editorView.markAsSaved()
295303
updateTabs()
296304
}
@@ -341,12 +349,19 @@ open class EditorHandlerActivity :
341349
prefs.getString(PREF_KEY_OPEN_FILES_CACHE, null)
342350
} ?: return@launch
343351

352+
if (editorViewModel.getOpenedFileCount() > 0) {
353+
// Returning to an in-memory session (e.g. after onPause/onStop). Replaying the
354+
// snapshot would be redundant and could interfere with dirty buffers and undo.
355+
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) }
356+
return@launch
357+
}
358+
344359
val cache = withContext(Dispatchers.Default) {
345360
Gson().fromJson(jsonCache, OpenedFilesCache::class.java)
346361
}
347362
onReadOpenedFilesCache(cache)
348363

349-
// Clear the preference so it's only loaded once on startup
364+
// Clear the preference so it's only loaded once per cold restore
350365
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) }
351366
} catch (err: Throwable) {
352367
log.error("Failed to reopen recently opened files", err)
@@ -747,6 +762,11 @@ open class EditorHandlerActivity :
747762
override fun onConfigurationChanged(newConfig: Configuration) {
748763
super.onConfigurationChanged(newConfig)
749764

765+
val safeContent = contentOrNull ?: return
766+
for (i in 0 until safeContent.editorContainer.childCount) {
767+
(safeContent.editorContainer.getChildAt(i) as? CodeEditorView)?.reapplyEditorDisplayPreferences()
768+
}
769+
750770
getCurrentEditor()?.editor?.apply {
751771
doOnNextLayout {
752772
cursor?.let { c -> ensurePositionVisible(c.leftLine, c.leftColumn, true) }
@@ -1069,17 +1089,20 @@ open class EditorHandlerActivity :
10691089
nameBuilder.addPath(it, it.path)
10701090
}
10711091

1072-
for (index in 0 until content.tabs.tabCount) {
1073-
val file = files.getOrNull(index) ?: continue
1092+
for (tabPos in 0 until content.tabs.tabCount) {
1093+
if (isPluginTab(tabPos)) continue
1094+
val fileIndex = getFileIndexForTabPosition(tabPos)
1095+
if (fileIndex < 0) continue
1096+
val file = files.getOrNull(fileIndex) ?: continue
10741097
val count = dupliCount[file.name] ?: 0
10751098

1076-
val isModified = getEditorAtIndex(index)?.isModified ?: false
1099+
val isModified = getEditorAtIndex(fileIndex)?.isModified ?: false
10771100
var name = if (count > 1) nameBuilder.getShortPath(file) else file.name
10781101
if (isModified) {
10791102
name = "*$name"
10801103
}
10811104

1082-
names[index] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon
1105+
names[tabPos] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon
10831106
}
10841107

10851108
withContext(Dispatchers.Main) {

app/src/main/java/com/itsaky/androidide/fragments/RecyclerViewFragment.kt

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ package com.itsaky.androidide.fragments
2020
import android.annotation.SuppressLint
2121
import android.os.Bundle
2222
import android.view.GestureDetector
23+
import android.view.LayoutInflater
2324
import android.view.MotionEvent
2425
import android.view.View
26+
import android.view.ViewGroup
2527
import androidx.recyclerview.widget.LinearLayoutManager
2628
import androidx.recyclerview.widget.RecyclerView
2729
import androidx.recyclerview.widget.RecyclerView.LayoutManager
28-
import com.itsaky.androidide.databinding.FragmentRecyclerviewBinding
30+
import androidx.viewbinding.ViewBinding
31+
import com.itsaky.androidide.R
2932
import com.itsaky.androidide.idetooltips.TooltipManager
3033

3134
/**
@@ -34,7 +37,7 @@ import com.itsaky.androidide.idetooltips.TooltipManager
3437
* @author Akash Yadav
3538
*/
3639
abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
37-
EmptyStateFragment<FragmentRecyclerviewBinding>(FragmentRecyclerviewBinding::inflate) {
40+
EmptyStateFragment<FragmentRecyclerviewManualBinding>(FragmentRecyclerviewManualBinding::inflate) {
3841
protected abstract val fragmentTooltipTag: String?
3942

4043
private var unsavedAdapter: A? = null
@@ -86,7 +89,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
8689
* Sets up the recycler view in the fragment.
8790
*/
8891
protected open fun onSetupRecyclerView() {
89-
binding.root.apply {
92+
binding.list.apply {
9093
layoutManager = onCreateLayoutManager()
9194
adapter = unsavedAdapter ?: onCreateAdapter()
9295
}
@@ -107,7 +110,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
107110

108111
onSetupRecyclerView()
109112

110-
binding.root.addOnItemTouchListener(touchListener)
113+
binding.list.addOnItemTouchListener(touchListener)
111114

112115
unsavedAdapter = null
113116

@@ -123,7 +126,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
123126
* Set the adapter for the [RecyclerView].
124127
*/
125128
fun setAdapter(adapter: A) {
126-
_binding?.root?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter }
129+
_binding?.list?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter }
127130
if (isAdded && view != null) {
128131
checkIsEmpty()
129132
}
@@ -142,6 +145,33 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
142145

143146
private fun checkIsEmpty() {
144147
if (!isAdded || isDetached) return
145-
isEmpty = _binding?.root?.adapter?.itemCount == 0
148+
isEmpty = _binding?.list?.adapter?.itemCount == 0
149+
}
150+
}
151+
152+
/**
153+
* Manual [ViewBinding] for [R.layout.fragment_recyclerview] so annotation processors (kapt) do not
154+
* depend on generated `FragmentRecyclerviewBinding` during stub analysis.
155+
*
156+
* Public (not internal/file-private): [RecyclerViewFragment] is public and Kotlin forbids a public
157+
* class from using a non-public type as a [EmptyStateFragment] type argument.
158+
*
159+
* [getRoot] returns [RecyclerView] (covariant override), matching generated view binding so
160+
* subclasses can use `binding.root.adapter` and other [RecyclerView] APIs.
161+
*/
162+
class FragmentRecyclerviewManualBinding(
163+
val list: RecyclerView,
164+
) : ViewBinding {
165+
override fun getRoot(): RecyclerView = list
166+
167+
companion object {
168+
fun inflate(
169+
inflater: LayoutInflater,
170+
parent: ViewGroup?,
171+
attachToParent: Boolean,
172+
): FragmentRecyclerviewManualBinding {
173+
val root = inflater.inflate(R.layout.fragment_recyclerview, parent, false) as RecyclerView
174+
return FragmentRecyclerviewManualBinding(root)
175+
}
146176
}
147177
}

app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,15 @@ class CodeEditorView(
567567
onPinLineNumbersPrefChanged()
568568
}
569569

570+
/**
571+
* Re-applies display-related preferences (font size, typeface, flags) after a configuration change
572+
* such as system font scale, so the editor activity can handle `fontScale` without being recreated.
573+
*/
574+
fun reapplyEditorDisplayPreferences() {
575+
if (_binding == null) return
576+
configureEditorIfNeeded()
577+
}
578+
570579
private fun onMagnifierPrefChanged() {
571580
binding.editor.getComponent(Magnifier::class.java).isEnabled =
572581
EditorPreferences.useMagnifier

0 commit comments

Comments
 (0)