Skip to content

ADFA-2845 | Fix IllegalStateException: Activity destroyed; binding not accessible#952

Merged
jatezzz merged 2 commits intostagefrom
fix/ADFA-2845-activity-destroyed-crash
Feb 12, 2026
Merged

ADFA-2845 | Fix IllegalStateException: Activity destroyed; binding not accessible#952
jatezzz merged 2 commits intostagefrom
fix/ADFA-2845-activity-destroyed-crash

Conversation

@jatezzz
Copy link
Collaborator

@jatezzz jatezzz commented Feb 9, 2026

Description

Introduced a contentOrNull safety property in BaseEditorActivity to prevent crashes when accessing the view binding after the Activity has been destroyed.

Refactored EditorHandlerActivity to use this property in various asynchronous callbacks (coroutines, EventBus subscribers, and UI thread runnables) to ensure the Activity state is valid before attempting to update the UI or access tabs.

Details

Logic-related fix. Prevents the application from crashing with java.lang.IllegalStateException: Activity destroyed; binding not accessible when background tasks (like saving files or plugin loading) finish after the user has exited the editor.

Before changes

Screen.Recording.2026-02-09.at.10.50.12.AM.mov
Screenshot 2026-02-09 at 10 52 06 AM

After changes

Screen.Recording.2026-02-09.at.11.45.15.AM.mov

Ticket

ADFA-2845

Observation

This fix addresses race conditions in saveAll, onReadOpenedFilesCache, createPluginTab, and EventBus events like onFileRenamed and onDocumentChange.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough
  • Fixed java.lang.IllegalStateException ("Activity destroyed; binding not accessible") by preventing access to view binding after an Activity is destroyed; addresses race conditions where background work (saving files, plugin loading, cache reads, EventBus handlers) completes after the editor is closed.
  • Added protected property contentOrNull: ContentEditorBinding? in BaseEditorActivity — a lifecycle-aware accessor that returns null if the Activity is destroyed or finishing.
  • Refactored EditorHandlerActivity to use contentOrNull across asynchronous and cross-thread code paths (coroutines, EventBus subscribers, UI-thread runnables), adding early-return guards in flows including saveAll, onReadOpenedFilesCache, createPluginTab, onFileRenamed, onDocumentChange, editor/tab updates, and plugin-tab interactions.
  • Fixed mapping of file index to tab position used during save operations so tab selection/updates target the correct tab.
  • Includes before/after images and a crash log demonstrating the IllegalStateException; linked ticket ADFA-2845.

Risks & best-practice notes:

  • ⚠️ Symptomatic fix: Null-guarding the binding prevents crashes but can mask deeper lifecycle issues. Prefer cancelling or scoping async work to the Activity lifecycle (e.g., lifecycleScope, explicit job cancellation) so work does not run after destruction.
  • ⚠️ Silent failures: Early returns when contentOrNull == null may drop important operations (saves, plugin tab creation) without visible feedback. Consider adding logging, retries, or user-facing fallbacks where operation loss is unacceptable.
  • ⚠️ Partial lifecycle enforcement: This reduces the crash surface but does not fully replace robust lifecycle management for background tasks; further hardening (cancelling coroutines, lifecycle-aware subscriptions) is recommended.

Walkthrough

Added a lifecycle-aware accessor contentOrNull to BaseEditorActivity and propagated guards in EditorHandlerActivity to short-circuit UI/editor/tab operations when the binding or activity is destroyed, preventing access to view binding during teardown.

Changes

Cohort / File(s) Summary
Lifecycle-safe binding
app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
Added protected val contentOrNull: ContentEditorBinding? that returns the content binding only when _binding exists and the activity is not destroyed/destroying.
Editor null-safety & UI guards
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
Added pervasive contentOrNull checks and early returns across lifecycle handlers, file/open/save flows, editor/tab updates, plugin-tab creation, and other UI paths; captures local content before performing UI/editor/tab operations to avoid NPEs.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Activity as BaseEditorActivity
participant Binding as ContentEditorBinding
participant Editor as EditorManager/Editor
participant Tabs as TabManager/UI
participant Plugin as Plugin/External

Activity->>Binding: access via contentOrNull?
alt content available
    Binding->>Editor: get editor for file / update document
    Editor->>Tabs: update tab text/state
    Tabs->>Plugin: create/update plugin tabs (UI thread)
else content missing
    Binding-->>Activity: null -> early return (no UI/editor ops)
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jomen-adfa
  • Daniel-ADFA
  • itsaky-adfa
  • dara-abijo-adfa

Poem

🐇 I hop through code with gentle care,
I check the binding, everywhere.
When activities sleep or say goodnight,
I guard the tabs and skip the fight,
A little hop — no crash in sight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically summarizes the main fix: addressing IllegalStateException crashes from accessing binding after Activity destruction.
Description check ✅ Passed The description is directly related to the changeset, explaining the contentOrNull property addition and refactoring to prevent lifecycle-related crashes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ADFA-2845-activity-destroyed-crash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (1)

965-992: ⚠️ Potential issue | 🟠 Major

content.tabs accessed from background thread in updateTabs — same crash risk.

Lines 978–989 run inside editorActivityScope.launch { ... }, which uses Dispatchers.Default. Accessing content.tabs.tabCount (line 978) and iterating tab children from a background thread has two problems:

  1. content (the throwing property) will throw IllegalStateException if the activity is destroyed — the same crash this PR fixes.
  2. Accessing Android views from a non-main thread is not safe.

The contentOrNull guard at line 992 only protects the withContext(Dispatchers.Main) block but not the preceding background work that also touches content.

Consider collecting the tab count on the main thread first, or restructuring so all view access is within the withContext(Dispatchers.Main) block.

🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt`:
- Around line 703-712: The code is using the file index variable "index" as a
tab position when calling content.tabs.getTabAt(index); replace that lookup with
the correct tab position by calling getTabPositionForFileIndex(index) and using
its result to fetch the tab, while keeping the rest of the logic (updating
editorViewModel.areFilesModified and trimming the '*' prefix) intact; ensure you
handle a null/invalid position the same way the current code handles a missing
tab (return@withContext) and reference content.tabs.getTabAt(...) and
getTabPositionForFileIndex(index) when making the change.
🧹 Nitpick comments (1)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (1)

949-963: Nit: inconsistent guard style — consider local capture for uniformity.

Other methods in this PR (e.g., getEditorForFile, saveResultInternal, confirmProjectClose) capture contentOrNull into a local val content, while here you check-and-discard then use the throwing content property on line 959. Both are safe on the main thread, but using the local capture pattern uniformly would be more consistent and slightly more robust.

Optional consistency improvement
 	fun onDocumentChange(event: DocumentChangeEvent) {
-		if (contentOrNull == null) return
+		val content = contentOrNull ?: return
 		editorViewModel.areFilesModified = true
 
 		val fileIndex = findIndexOfEditorByFile(event.file.toFile())

@jatezzz jatezzz requested a review from a team February 9, 2026 20:42
@jatezzz jatezzz force-pushed the fix/ADFA-2845-activity-destroyed-crash branch from ca5dc9d to edf9801 Compare February 10, 2026 13:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (2)

671-718: ⚠️ Potential issue | 🔴 Critical

Remaining crash path: getEditorAtIndex called from IO thread before the contentOrNull guard.

The contentOrNull guard on line 704 only protects the withContext(Dispatchers.Main) block. However, line 679 calls getEditorAtIndex(index) while still on the IO thread (via saveAllAsyncsaveAllResultsaveResultInternal).

getEditorAtIndex (line 447) calls getTabPositionForFileIndex(index) which accesses the throwing content.tabs.tabCount property. If the activity is destroyed during an IO-thread save, this will throw the same IllegalStateException the PR intends to fix.

Suggested fix
 	override fun getEditorAtIndex(index: Int): CodeEditorView? {
+		val content = contentOrNull ?: return null
 		val tabPosition = getTabPositionForFileIndex(index)
 		if (tabPosition < 0) return null
-		val child = _binding?.content?.editorContainer?.getChildAt(tabPosition) ?: return null
+		val child = content.editorContainer.getChildAt(tabPosition) ?: return null
 		return if (child is CodeEditorView) child else null
 	}

And getTabPositionForFileIndex also needs guarding:

 	private fun getTabPositionForFileIndex(fileIndex: Int): Int {
 		if (fileIndex < 0) return -1
+		val content = contentOrNull ?: return -1
 		var tabPos = 0
 		var fileCount = 0
 		while (tabPos < content.tabs.tabCount) {

967-1009: ⚠️ Potential issue | 🔴 Critical

Same background-thread access issue in updateTabs.

Lines 980 and 984 access content.tabs.tabCount and getEditorAtIndex(index) from the background editorActivityScope (Dispatchers.Default) thread. If the activity is destroyed while this coroutine is running, content will throw IllegalStateException.

The guard at line 994 only protects the withContext(Dispatchers.Main) portion. The background portion needs its own guard:

Suggested fix
 	private fun updateTabs() {
 		editorActivityScope.launch {
+			val content = contentOrNull ?: return@launch
 			val files = editorViewModel.getOpenedFiles()
 			val dupliCount = mutableMapOf<String, Int>()
🧹 Nitpick comments (1)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (1)

950-965: Consider using local capture for consistency.

Line 952 checks contentOrNull == null but subsequent lines (961, 962, 964) access the throwing content property directly. While this is safe on the main thread (no interleaving), it's inconsistent with the local-capture pattern used elsewhere (e.g., lines 704, 937, 994). Using val content = contentOrNull ?: return would be more uniform and resilient to future refactoring.

Suggested change
 	`@Subscribe`(threadMode = ThreadMode.MAIN)
 	fun onDocumentChange(event: DocumentChangeEvent) {
-		if (contentOrNull == null) return
+		val content = contentOrNull ?: return
 		editorViewModel.areFilesModified = true
 
 		val fileIndex = findIndexOfEditorByFile(event.file.toFile())

@jatezzz jatezzz force-pushed the fix/ADFA-2845-activity-destroyed-crash branch from edf9801 to fc2c92f Compare February 12, 2026 14:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (3)

349-358: 🛠️ Refactor suggestion | 🟠 Major

Inconsistent contentOrNull usage — prefer capturing into a local variable.

Line 351 checks contentOrNull == null but lines 352–358 access content (the non-null property) directly. If the activity is destroyed between the check and the access, the crash can still occur. The same TOCTOU pattern appears at lines 632, 952, and 1318.

Use the local-capture pattern consistently (as you already do at lines 704, 937, 994, 1074, 1293):

Proposed fix (line 351)
 		withContext(Dispatchers.Main) {
-			if (contentOrNull == null) return@withContext
+			val content = contentOrNull ?: return@withContext
 			existingFiles.forEach { file ->
 				openFile(File(file.filePath), file.selection)
 			}

Apply the same change at lines 632, 952, and 1318 for consistency and safety.


936-941: ⚠️ Potential issue | 🟡 Minor

File index compared against tab count (includes plugin tabs).

Line 939 checks index >= content.tabs.tabCount, but index is a file index returned by findIndexOfEditorByFile. With plugin tabs, tabCount can exceed the file count, so this guard is too loose. It should compare against editorViewModel.getOpenedFileCount().

Proposed fix
 	fun onFileRenamed(event: FileRenameEvent) {
 		val content = contentOrNull ?: return
 		val index = findIndexOfEditorByFile(event.file)
-		if (index < 0 || index >= content.tabs.tabCount) {
+		if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
 			return
 		}

1120-1124: ⚠️ Potential issue | 🟡 Minor

createPluginTab returns true before runOnUiThread executes.

The method returns true at line 1120 immediately, but the actual tab creation happens asynchronously inside runOnUiThread (line 1073). If the tab creation fails (e.g., contentOrNull is null), the caller still sees success. This is pre-existing, but worth noting since you're already modifying this method.

@jatezzz jatezzz merged commit 8aa0862 into stage Feb 12, 2026
2 checks passed
@jatezzz jatezzz deleted the fix/ADFA-2845-activity-destroyed-crash branch February 12, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments