Skip to content

ADFA-2594: Fix plugin fragment crash on configuration change#853

Merged
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-2594
Jan 20, 2026
Merged

ADFA-2594: Fix plugin fragment crash on configuration change#853
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-2594

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

When UI settings (font size, dark mode) change, Android recreates the
Activity and FragmentManager tries to restore plugin fragments using
the default classloader, which doesn't have access to plugin DEX classes.

  • Add PluginFragmentFactory to instantiate plugin fragments using the correct DexClassLoader during fragment restoration
  • Register plugin classloaders when fragments are first created
  • Save/restore open plugin tab IDs via SharedPreferences to persist tab UI across configuration changes

  When UI settings (font size, dark mode) change, Android recreates the
  Activity and FragmentManager tries to restore plugin fragments using
  the default classloader, which doesn't have access to plugin DEX classes.

  - Add PluginFragmentFactory to instantiate plugin fragments using the
    correct DexClassLoader during fragment restoration
  - Register plugin classloaders when fragments are first created
  - Save/restore open plugin tab IDs via SharedPreferences to persist
    tab UI across configuration changes
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

Caution

Review failed

The head commit changed during the review from 5feab84 to db11c9a.

📝 Walkthrough

Walkthrough

Adds persistent plugin-tab state and a plugin-aware FragmentFactory. Plugin class loaders are exposed and registered so plugin fragments can be instantiated via their plugin ClassLoaders; activity, adapter, and tab manager are wired to save/restore and register/unregister plugin fragment class loaders.

Changes

Cohort / File(s) Summary
Editor Activity: persistence & factory
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
Added PREF_KEY_OPEN_PLUGIN_TABS, saveOpenedPluginTabs() and restoreOpenedPluginTabs() (Gson + SharedPreferences) and setupPluginFragmentFactory(); save/restore plugin tab state on lifecycle events and install PluginFragmentFactory.
Bottom-sheet adapter: plugin tab wiring
app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt
Introduced PluginTabData pairing TabItem with UIExtension, added pluginExtensions map, deferred fragment loading to plugin-aware flow, and registered plugin fragment class loaders via PluginFragmentFactory.
Plugin fragment factory
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt
New PluginFragmentFactory subclass of FragmentFactory with companion APIs to register/unregister per-plugin ClassLoaders and to instantiate fragments using registered plugin ClassLoaders (falls back to default factory).
PluginManager: classloader access & cleanup
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
Added getClassLoaderForPlugin() / getClassLoaderForPluginId() accessors and updated unload flow to call PluginFragmentFactory.unregisterAllClassLoadersForPlugin(pluginId).
Plugin editor tab manager: register loaders
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt
Cached pluginManagerRef, added registerFragmentClassLoader() to obtain plugin ClassLoader and register fragment class with PluginFragmentFactory, updated loadPluginTabs to store reference, extended TabSelectionListener with onTabClosed(tabId: String).

Sequence Diagrams

sequenceDiagram
    participant Activity as EditorHandlerActivity
    participant FragMgr as FragmentManager
    participant PluginFF as PluginFragmentFactory
    participant Adapter as EditorBottomSheetTabAdapter
    participant TabMgr as PluginEditorTabManager
    participant PluginMgr as PluginManager

    Activity->>Activity: onCreate()
    Activity->>PluginFF: setupPluginFragmentFactory() (install factory)
    Activity->>Activity: restoreOpenedPluginTabs()

    Adapter->>PluginMgr: getClassLoaderForPlugin(plugin)
    PluginMgr-->>Adapter: ClassLoader?
    Adapter->>PluginFF: registerPluginClassLoader(pluginId, classLoader, [className])

    TabMgr->>PluginMgr: loadPluginTabs() (store pluginManagerRef)
    TabMgr->>TabMgr: getOrCreateTabFragment(extension)
    TabMgr->>PluginMgr: getClassLoaderForPlugin(extension.plugin)
    PluginMgr-->>TabMgr: ClassLoader?
    TabMgr->>PluginFF: registerPluginClassLoader(pluginId, classLoader, [className])
    TabMgr->>FragMgr: request instantiate(className)
    FragMgr->>PluginFF: instantiate(classLoader, className)
    PluginFF->>PluginFF: hasClassLoaderForFragment(className)?
    alt Registered
        PluginFF->>PluginFF: load class via plugin ClassLoader & construct
        PluginFF-->>FragMgr: return plugin Fragment
    else Unregistered
        PluginFF->>FragMgr: delegate to default factory
    end

    Activity->>Activity: onPause()
    Activity->>Activity: saveOpenedPluginTabs()
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Reviewers

  • dara-abijo-adfa
  • jatezzz

Poem

🐰 I hopped in code where tabs persist,

Classloaders fetch what plugins insist.
Factories register with a gentle thump,
Fragments wake from a saved tab jump.
Hooray — plugins restored, I munch a carrot crisp!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% 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 clearly and specifically describes the main fix: addressing plugin fragment crashes during configuration changes, which matches the primary objective of the changeset.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem context and detailing the three main solutions implemented across multiple files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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
Copy Markdown
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: 2

🤖 Fix all issues with AI agents
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt`:
- Around line 14-46: The shared mutable map pluginClassLoaders is not
thread-safe; replace it with a thread-safe implementation (e.g., a
java.util.concurrent.ConcurrentHashMap) or wrap it with
Collections.synchronizedMap and update its declaration accordingly so all
accesses in registerPluginClassLoader, unregisterPluginClassLoader,
hasClassLoaderForFragment, getClassLoaderForFragment, and clearAllClassLoaders
are safe; ensure you do not rely on non-atomic compound operations (if you do,
add explicit synchronization around those operations).
- Around line 24-30: Call PluginFragmentFactory.unregisterPluginClassLoader(...)
from PluginManager.unloadPlugin() to remove classloaders for that plugin and
avoid leaks: inside PluginManager.unloadPlugin() (after or alongside the
existing PluginFragmentHelper.unregisterPluginContext(...) call), gather the
list of fragment class names associated with the plugin (e.g., from the plugin's
metadata/manifest or stored registration used by registerPluginClassLoader
calls) and invoke PluginFragmentFactory.unregisterPluginClassLoader(pluginId,
fragmentClassNames) so entries are removed from the pluginClassLoaders map;
ensure you pass the exact fragment class name list that was registered when the
plugin was loaded.
🧹 Nitpick comments (4)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt (2)

32-53: Consider using direct singleton access instead of caching the reference.

Storing pluginManagerRef creates a dependency on the caller remembering to call loadPluginTabs() before getOrCreateTabFragment(). While the current code handles this with a warning, consider accessing PluginManager.getInstance() directly in registerFragmentClassLoader for consistency with other similar code in EditorBottomSheetTabAdapter.

♻️ Suggested approach
-    private var pluginManagerRef: PluginManager? = null
...
     private fun registerFragmentClassLoader(extension: EditorTabExtension, fragment: Fragment) {
-        val pluginManager = pluginManagerRef ?: run {
+        val pluginManager = PluginManager.getInstance() ?: run {
             logger.warn("PluginManager not available, cannot register fragment classloader")
             return
         }

137-156: Consider using debug level for successful registration logs.

The logger.info on line 152 will produce logs for every plugin fragment creation. For production, logger.debug would be more appropriate to avoid log noise, consistent with the debug-level logging used in EditorBottomSheetTabAdapter.registerPluginFragmentClassLoader.

♻️ Suggested change
-            logger.info("Registered classloader for fragment {} from plugin {}", fragmentClassName, pluginId)
+            logger.debug("Registered classloader for fragment {} from plugin {}", fragmentClassName, pluginId)
app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt (1)

219-229: Consider using the class logger for consistency.

The method uses Log.d directly while the rest of the class uses log (the SLF4J logger). For consistency and to benefit from any logging configuration, consider using the class logger.

♻️ Suggested change
-		Log.d("EditorHandlerActivity", "Saved open plugin tabs: $openPluginTabIds")
+		log.debug("Saved open plugin tabs: {}", openPluginTabIds)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt (1)

49-67: Consider using Kotlin's reflection or updated Java API.

constructor.isAccessible = true is deprecated. While it still works, consider using constructor.trySetAccessible() (Java 9+) or Kotlin's isAccessible property which handles the deprecation more gracefully.

♻️ Suggested change
                 val fragmentClass = pluginClassLoader.loadClass(className)
                 val constructor = fragmentClass.getDeclaredConstructor()
-                constructor.isAccessible = true
+                if (!constructor.trySetAccessible()) {
+                    Log.w(TAG, "Could not make constructor accessible for: $className")
+                }
                 constructor.newInstance() as Fragment

Or simply rely on the fact that newInstance() will throw if inaccessible, which your catch block already handles.

@Daniel-ADFA Daniel-ADFA merged commit 1daa343 into stage Jan 20, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-2594 branch January 20, 2026 11:41
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