Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
Expand Down Expand Up @@ -96,6 +97,7 @@ open class EditorHandlerActivity :

companion object {
const val PREF_KEY_OPEN_FILES_CACHE = "open_files_cache_v1"
const val PREF_KEY_OPEN_PLUGIN_TABS = "open_plugin_tabs_v1"
}

protected val isOpenedFilesSaved = AtomicBoolean(false)
Expand Down Expand Up @@ -141,6 +143,7 @@ open class EditorHandlerActivity :
}

override fun onCreate(savedInstanceState: Bundle?) {
setupPluginFragmentFactory()
mBuildEventListener.setActivity(this)
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -208,10 +211,23 @@ open class EditorHandlerActivity :
ActionContextProvider.clearActivity()
if (!isOpenedFilesSaved.get()) {
saveOpenedFiles()
saveOpenedPluginTabs()
saveAllAsync(notify = false)
}
}

private fun saveOpenedPluginTabs() {
val prefs = (application as BaseApplication).prefManager
val openPluginTabIds = pluginTabIndices.keys.toList()
if (openPluginTabIds.isEmpty()) {
prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null)
return
}
val json = Gson().toJson(openPluginTabIds)
prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, json)
Log.d("EditorHandlerActivity", "Saved open plugin tabs: $openPluginTabIds")
}

override fun onResume() {
super.onResume()
ActionContextProvider.setActivity(this)
Expand Down Expand Up @@ -298,6 +314,28 @@ open class EditorHandlerActivity :
log.error("Failed to reopen recently opened files", err)
}
}

restoreOpenedPluginTabs()
}

private fun restoreOpenedPluginTabs() {
try {
val prefs = (application as BaseApplication).prefManager
val json = prefs.getString(PREF_KEY_OPEN_PLUGIN_TABS, null) ?: return

val tabIds = Gson().fromJson(json, Array<String>::class.java)?.toList() ?: return
Log.d("EditorHandlerActivity", "Restoring plugin tabs: $tabIds")

tabIds.forEach { tabId ->
if (!pluginTabIndices.containsKey(tabId)) {
selectPluginTabById(tabId)
}
}

prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null)
} catch (e: Exception) {
Log.e("EditorHandlerActivity", "Failed to restore plugin tabs", e)
}
}

private fun onReadOpenedFilesCache(cache: OpenedFilesCache?) {
Expand Down Expand Up @@ -1042,6 +1080,16 @@ open class EditorHandlerActivity :
}
}

private fun setupPluginFragmentFactory() {
try {
val defaultFactory = supportFragmentManager.fragmentFactory
supportFragmentManager.fragmentFactory = PluginFragmentFactory(defaultFactory)
Log.d("EditorHandlerActivity", "PluginFragmentFactory installed")
} catch (e: Exception) {
Log.e("EditorHandlerActivity", "Failed to setup PluginFragmentFactory", e)
}
}

fun loadPluginTabs() {
try {
val pluginManager =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.itsaky.androidide.fragments.output.IDELogFragment
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.plugins.extensions.TabItem
import com.itsaky.androidide.plugins.extensions.UIExtension
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.FeatureFlags
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -124,6 +125,7 @@ class EditorBottomSheetTabAdapter(

private val tabs = MutableList(allTabs.size) { allTabs[it] }
private val pluginFragmentFactories = mutableMapOf<Long, () -> Fragment>()
private val pluginExtensions = mutableMapOf<Long, UIExtension>()

init {
addPluginTabs()
Expand All @@ -134,6 +136,7 @@ class EditorBottomSheetTabAdapter(
if (size == 0) return
tabs.clear()
pluginFragmentFactories.clear()
pluginExtensions.clear()
notifyDataSetChanged()
}

Expand Down Expand Up @@ -233,7 +236,9 @@ class EditorBottomSheetTabAdapter(
// Check if this is a plugin fragment
val pluginFactory = pluginFragmentFactories[tab.itemId]
if (pluginFactory != null) {
return pluginFactory.invoke()
val fragment = pluginFactory.invoke()
registerPluginFragmentClassLoader(tab.itemId, fragment)
return fragment
}

// Regular fragment creation
Expand All @@ -246,6 +251,27 @@ class EditorBottomSheetTabAdapter(
}
}

private fun registerPluginFragmentClassLoader(tabItemId: Long, fragment: Fragment) {
try {
val plugin = pluginExtensions[tabItemId] ?: return
val pluginManager = getPluginManager() ?: return

val classLoader = pluginManager.getClassLoaderForPlugin(plugin)
if (classLoader != null) {
val fragmentClassName = fragment.javaClass.name
val pluginId = pluginManager.getPluginIdForInstance(plugin) ?: "unknown"
PluginFragmentFactory.registerPluginClassLoader(
pluginId,
classLoader,
listOf(fragmentClassName)
)
logger.debug("Registered classloader for bottom sheet fragment {} from plugin {}", fragmentClassName, pluginId)
}
} catch (e: Exception) {
logger.error("Failed to register plugin fragment classloader", e)
}
}

override fun getItemCount(): Int = tabs.size

fun getTitle(position: Int): String? = tabs[position].title
Expand Down Expand Up @@ -303,6 +329,8 @@ class EditorBottomSheetTabAdapter(

fun getTooltipTag(position: Int): String? = allTabs[position].tooltipTag

private data class PluginTabData(val tabItem: TabItem, val plugin: UIExtension)

private fun addPluginTabs() {
try {
val pluginManager = getPluginManager()
Expand All @@ -314,7 +342,7 @@ class EditorBottomSheetTabAdapter(
val loadedPlugins = pluginManager.getAllPluginInstances()
logger.debug("Found {} loaded plugins for tab registration", loadedPlugins.size)

val pluginTabs = mutableListOf<TabItem>()
val pluginTabs = mutableListOf<PluginTabData>()

for (plugin in loadedPlugins) {
try {
Expand All @@ -330,7 +358,7 @@ class EditorBottomSheetTabAdapter(

for (tabItem in tabItems) {
if (tabItem.isEnabled && tabItem.isVisible) {
pluginTabs.add(tabItem)
pluginTabs.add(PluginTabData(tabItem, plugin))
logger.debug("Added plugin tab: {} - {}", tabItem.id, tabItem.title)
}
}
Expand All @@ -346,26 +374,27 @@ class EditorBottomSheetTabAdapter(
}

// Sort tabs by order
pluginTabs.sortBy { it.order }
pluginTabs.sortBy { it.tabItem.order }

// Add plugin tabs to the adapter at the end
val startIndex = allTabs.size
for ((index, tabItem) in pluginTabs.withIndex()) {
for ((index, data) in pluginTabs.withIndex()) {
val tab =
Tab(
title = tabItem.title,
title = data.tabItem.title,
fragmentClass = Fragment::class.java, // Placeholder, actual fragment from factory
itemId = startIndex + index + 1000L, // Offset to avoid conflicts
tooltipTag = null,
)

// Store the fragment factory for later use
pluginFragmentFactories[tab.itemId] = tabItem.fragmentFactory
// Store the fragment factory and the extension for later use
pluginFragmentFactories[tab.itemId] = data.tabItem.fragmentFactory
pluginExtensions[tab.itemId] = data.plugin

allTabs.add(tab)
tabs.add(tab)

logger.debug("Registered plugin tab at index {}: {}", startIndex + index, tabItem.title)
logger.debug("Registered plugin tab at index {}: {}", startIndex + index, data.tabItem.title)
}
} catch (e: Exception) {
logger.error("Error in plugin tab integration: {}", e.message, e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.app.Activity
import android.content.Context
import com.itsaky.androidide.plugins.*
import com.itsaky.androidide.plugins.base.PluginFragmentHelper
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
import com.itsaky.androidide.plugins.services.IdeProjectService
import com.itsaky.androidide.plugins.services.IdeUIService
import com.itsaky.androidide.plugins.services.IdeBuildService
Expand Down Expand Up @@ -441,6 +442,9 @@ class PluginManager private constructor(
// Unregister the plugin's resource context
PluginFragmentHelper.unregisterPluginContext(pluginId)

// Unregister all fragment classloaders for this plugin to avoid leaks
PluginFragmentFactory.unregisterAllClassLoadersForPlugin(pluginId)

logger.info("Unloaded plugin: $pluginId")
return true
} catch (e: Exception) {
Expand Down Expand Up @@ -563,6 +567,16 @@ class PluginManager private constructor(
?.key
}

fun getClassLoaderForPlugin(plugin: IPlugin): ClassLoader? {
return loadedPlugins.values
.find { it.plugin === plugin }
?.classLoader
}

fun getClassLoaderForPluginId(pluginId: String): ClassLoader? {
return loadedPlugins[pluginId]?.classLoader
}

fun enablePlugin(pluginId: String): Boolean {
val loadedPlugin = loadedPlugins[pluginId] ?: return false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,8 @@ class PluginDocumentationManager(private val context: Context) {
plugin: DocumentationExtension
): Boolean = withContext(Dispatchers.IO) {
if (!isDatabaseAvailable()) {
Log.w(TAG, "Documentation database not available, skipping verification for $pluginId")
return@withContext false
Log.d(TAG, "Documentation database does not exist, initializing for $pluginId...")
initialize()
}

val isInstalled = isPluginDocumentationInstalled(pluginId)
Expand All @@ -521,11 +521,15 @@ class PluginDocumentationManager(private val context: Context) {
suspend fun verifyAllPluginDocumentation(
plugins: Map<String, DocumentationExtension>
): Int = withContext(Dispatchers.IO) {
if (!isDatabaseAvailable()) {
Log.w(TAG, "Documentation database not available, skipping verification")
if (plugins.isEmpty()) {
return@withContext 0
}

if (!isDatabaseAvailable()) {
Log.d(TAG, "Documentation database does not exist, initializing...")
initialize()
}

var recreatedCount = 0

for ((pluginId, plugin) in plugins) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.itsaky.androidide.plugins.manager.fragment

import android.util.Log
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import java.util.concurrent.ConcurrentHashMap

class PluginFragmentFactory(
private val defaultFactory: FragmentFactory
) : FragmentFactory() {

companion object {
private const val TAG = "PluginFragmentFactory"

private val pluginClassLoaders = ConcurrentHashMap<String, ClassLoader>()
private val pluginFragmentClasses = ConcurrentHashMap<String, MutableSet<String>>()

@JvmStatic
fun registerPluginClassLoader(pluginId: String, classLoader: ClassLoader, fragmentClassNames: List<String>) {
val fragmentSet = pluginFragmentClasses.computeIfAbsent(pluginId) {
ConcurrentHashMap.newKeySet()
}
fragmentClassNames.forEach { className ->
pluginClassLoaders[className] = classLoader
fragmentSet.add(className)
Log.d(TAG, "Registered classloader for fragment: $className (plugin: $pluginId)")
}
}

@JvmStatic
fun unregisterPluginClassLoader(pluginId: String, fragmentClassNames: List<String>) {
val fragmentSet = pluginFragmentClasses[pluginId]
fragmentClassNames.forEach { className ->
pluginClassLoaders.remove(className)
fragmentSet?.remove(className)
Log.d(TAG, "Unregistered classloader for fragment: $className (plugin: $pluginId)")
}
}

@JvmStatic
fun unregisterAllClassLoadersForPlugin(pluginId: String) {
val fragmentClassNames = pluginFragmentClasses.remove(pluginId) ?: return
fragmentClassNames.forEach { className ->
pluginClassLoaders.remove(className)
Log.d(TAG, "Unregistered classloader for fragment: $className (plugin: $pluginId)")
}
Log.d(TAG, "Unregistered all classloaders for plugin: $pluginId (${fragmentClassNames.size} fragments)")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@JvmStatic
fun hasClassLoaderForFragment(className: String): Boolean {
return pluginClassLoaders.containsKey(className)
}

@JvmStatic
fun getClassLoaderForFragment(className: String): ClassLoader? {
return pluginClassLoaders[className]
}

@JvmStatic
fun clearAllClassLoaders() {
pluginClassLoaders.clear()
pluginFragmentClasses.clear()
Log.d(TAG, "Cleared all plugin fragment classloaders")
}
}

override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
val pluginClassLoader = pluginClassLoaders[className]

if (pluginClassLoader != null) {
Log.d(TAG, "Instantiating plugin fragment with plugin classloader: $className")
return try {
val fragmentClass = pluginClassLoader.loadClass(className)
val constructor = fragmentClass.getDeclaredConstructor()
constructor.isAccessible = true
constructor.newInstance() as Fragment
} catch (e: Exception) {
Log.e(TAG, "Failed to instantiate plugin fragment: $className", e)
throw e
}
}

Log.d(TAG, "Using default factory for fragment: $className")
return defaultFactory.instantiate(classLoader, className)
}
}
Loading