Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
android:theme="@style/Theme.AndroidIDE" />
<activity
android:name=".activities.editor.EditorActivityKt"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|fontScale|density|uiMode"
android:launchMode="singleTask"
Comment on lines 95 to 97
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When an Android activity handles uiModeanddensityviaandroid:configChanges, what theme/resource updates must it perform manually in onConfigurationChanged to avoid stale dark-mode and density-dependent UI?

💡 Result:

When an Android activity declares android:configChanges="uiMode|density" in its manifest, it must manually update its UI in onConfigurationChanged(Configuration newConfig) to reflect changes in dark mode (uiMode) and screen density, as the system does not recreate the activity. Key manual updates required: 1. Reload layouts and resources: Call setContentView(R.layout.activity_main) to reload the layout from the appropriate resource folder (e.g., layout-night for dark mode, density-specific drawables). This ensures density-dependent bitmaps and dimension resources are updated. 2. Re-apply theme attributes: For views using theme colors/text colors (e.g., ?attr/colorPrimary), reload backgrounds, text colors, etc., via view.setBackgroundResource or ContextCompat.getColor after setContentView, as theme application may not fully update. 3. Update custom views: Recreate or refresh any programmatically created views, custom views, or adapters that depend on density (e.g., sizes from getResources.getDimension) or uiMode (night mode checks). 4. Refresh specific elements: For images/backgrounds, explicitly set view.setImageResource(R.drawable.icon) or view.setBackgroundResource(R.drawable.bg) to pick the correct density or night-qualified variant. 5. Handle WebView or special views: Manually update configuration for WebView using Resources.updateConfiguration if uiMode resets, or recreate it. The activity's Resources object is automatically updated with the new Configuration, so subsequent getResources.getDrawable calls return correct variants. However, existing View hierarchy caches old resource IDs/bitmaps, leading to stale UI unless explicitly refreshed. Official docs warn: "you are responsible for resetting any elements for which you provide alternatives." Examples include reassigning orientation-specific images; extend to uiMode/density. Avoid configChanges unless necessary (e.g., video playback); prefer activity recreation for automatic resource reloading. Use ViewModels for state preservation.

Citations:


🏁 Script executed:

# Find EditorActivityKt file
fd -t f "EditorActivityKt"

Repository: appdevforall/CodeOnTheGo

Length of output: 145


🏁 Script executed:

# Find EditorActivity (might be .java or .kt version)
fd -t f "EditorActivity"

Repository: appdevforall/CodeOnTheGo

Length of output: 551


🏁 Script executed:

# Search for the onConfigurationChanged implementation in EditorActivity files
rg "onConfigurationChanged" --type kotlin --type java -A 15

Repository: appdevforall/CodeOnTheGo

Length of output: 17453


🏁 Script executed:

rg "reapplyEditorDisplayPreferences" -A 20 --type kotlin --type java

Repository: appdevforall/CodeOnTheGo

Length of output: 4160


🏁 Script executed:

# Find CodeEditorView to check initialization
fd -t f "CodeEditorView"

Repository: appdevforall/CodeOnTheGo

Length of output: 128


🏁 Script executed:

# Check configureEditorIfNeeded and initialization in CodeEditorView
rg "configureEditorIfNeeded|SchemeAndroidIDE" app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt -A 10 -B 2

Repository: appdevforall/CodeOnTheGo

Length of output: 2704


🏁 Script executed:

# Check CodeEditorView constructor/initialization for theme/density-dependent state
head -100 app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt

Repository: appdevforall/CodeOnTheGo

Length of output: 4255


🏁 Script executed:

# Check if SchemeAndroidIDE.newInstance is called again or if theme is reloaded
rg "SchemeAndroidIDE|colorScheme.*=" app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt -B 2 -A 2

Repository: appdevforall/CodeOnTheGo

Length of output: 519


🏁 Script executed:

# Check configureEditorIfNeeded full implementation and what it covers
rg "private fun configureEditorIfNeeded" app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt -A 20

Repository: appdevforall/CodeOnTheGo

Length of output: 748


🏁 Script executed:

# Search for density or uiMode related handling in CodeEditorView
rg "density|uiMode|theme|dark" app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt -i

Repository: appdevforall/CodeOnTheGo

Length of output: 50


🏁 Script executed:

# Check EditorActivityKt hierarchy and if it calls onConfigurationChanged from parent
cat app/src/main/java/com/itsaky/androidide/activities/editor/EditorActivityKt.kt | head -50

Repository: appdevforall/CodeOnTheGo

Length of output: 953


🏁 Script executed:

# Check the full initialization block where colorScheme is set
rg "colorScheme = SchemeAndroidIDE" app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt -B 10 -A 5

Repository: appdevforall/CodeOnTheGo

Length of output: 548


Don't opt out of uiMode/density recreation unless those resources are fully rebound.

The onConfigurationChanged() handler only reapplies editor preferences (font size, typeface) via configureEditorIfNeeded(). It does not recreate theme-dependent state like SchemeAndroidIDE.newInstance(context) or re-apply density-dependent values initialized during CodeEditorView construction (e.g., SizeUtils.dp2px for divider width). Dark-mode or display-scale changes will leave the color scheme and density-dependent drawables stale.

The docstring in reapplyEditorDisplayPreferences() explicitly states it handles "system font scale," which is the scope of this PR. Keep fontScale and let uiMode and density trigger activity recreation until the full UI rebinding is implemented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/AndroidManifest.xml` around lines 95 - 97, The manifest opts out
of uiMode and density configuration changes for the activity declared as
android:name=".activities.editor.EditorActivityKt", but onConfigurationChanged()
only calls configureEditorIfNeeded()/reapplyEditorDisplayPreferences() and does
not rebind theme/density-dependent state (e.g.,
SchemeAndroidIDE.newInstance(context), CodeEditorView initialization or
SizeUtils.dp2px-based drawables), so remove uiMode and density from the
android:configChanges attribute (leave fontScale so system font-scale handling
remains) to force activity recreation until full UI rebinding is implemented.

android:windowSoftInputMode="adjustResize" />
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ open class EditorHandlerActivity :
loadPluginTabs()
}

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

Expand All @@ -271,26 +274,31 @@ open class EditorHandlerActivity :
invalidateOptionsMenu()
}

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

lifecycleScope.launch(Dispatchers.IO) {
// Check each open file
openFiles.forEach { file ->
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()
Comment on lines +277 to 302
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

canUndo()/canRedo() is too broad as the reload gate.

CodeEditorView.markAsSaved() only calls markUnmodified(), and IDEEditor.markUnmodified() only clears isModified (see app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt:318-322 and editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt:662-673). After any manual save, Line 297 can still stay true, so external updates are silently ignored for a clean editor and the user keeps editing a stale buffer. If undo history must be preserved, this path needs an explicit conflict prompt instead of an early return.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt`
around lines 277 - 302, The reload is incorrectly gated by
IDEEditor.canUndo()/canRedo() so clean buffers that still have undo history
(after markAsSaved()/markUnmodified()) are skipped; in
checkForExternalFileChanges() remove or replace the early return that checks
ideEditor.canUndo() || ideEditor.canRedo() so that when editorView.isModified is
false the file is reloaded (call ideEditor.setText(newContent) and
editorView.markAsSaved()); if you must preserve undo history instead of
discarding it, replace that early-return with an explicit user conflict
prompt/confirmation flow (show dialog) rather than silently ignoring the
external change—refer to checkForExternalFileChanges(),
CodeEditorView.markAsSaved(), and IDEEditor.canUndo()/canRedo()/markUnmodified()
when making the change.

updateTabs()
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

private var unsavedAdapter: A? = null
Expand Down Expand Up @@ -86,7 +89,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
* Sets up the recycler view in the fragment.
*/
protected open fun onSetupRecyclerView() {
binding.root.apply {
binding.list.apply {
layoutManager = onCreateLayoutManager()
adapter = unsavedAdapter ?: onCreateAdapter()
}
Expand All @@ -107,7 +110,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :

onSetupRecyclerView()

binding.root.addOnItemTouchListener(touchListener)
binding.list.addOnItemTouchListener(touchListener)

unsavedAdapter = null

Expand All @@ -123,7 +126,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
* 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()
}
Expand All @@ -142,6 +145,33 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :

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)
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading