From adf59dd825ff6ba14b15f0f7decf521a4278cbf2 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Sun, 18 Jan 2026 23:58:34 +0100 Subject: [PATCH 1/3] Fix plugin fragment crash on configuration change 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 --- .../editor/EditorHandlerActivity.kt | 48 +++++++++++++ .../adapters/EditorBottomSheetTabAdapter.kt | 47 ++++++++++--- .../plugins/manager/core/PluginManager.kt | 10 +++ .../manager/fragment/PluginFragmentFactory.kt | 68 +++++++++++++++++++ .../manager/ui/PluginEditorTabManager.kt | 27 ++++++++ 5 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index bad679c2b2..06fd60d335 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -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 @@ -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) @@ -141,6 +143,7 @@ open class EditorHandlerActivity : } override fun onCreate(savedInstanceState: Bundle?) { + setupPluginFragmentFactory() mBuildEventListener.setActivity(this) super.onCreate(savedInstanceState) @@ -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) @@ -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::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?) { @@ -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 = diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt index 37ec962d40..26fffbf151 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt @@ -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 @@ -124,6 +125,7 @@ class EditorBottomSheetTabAdapter( private val tabs = MutableList(allTabs.size) { allTabs[it] } private val pluginFragmentFactories = mutableMapOf Fragment>() + private val pluginExtensions = mutableMapOf() init { addPluginTabs() @@ -134,6 +136,7 @@ class EditorBottomSheetTabAdapter( if (size == 0) return tabs.clear() pluginFragmentFactories.clear() + pluginExtensions.clear() notifyDataSetChanged() } @@ -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 @@ -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 @@ -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() @@ -314,7 +342,7 @@ class EditorBottomSheetTabAdapter( val loadedPlugins = pluginManager.getAllPluginInstances() logger.debug("Found {} loaded plugins for tab registration", loadedPlugins.size) - val pluginTabs = mutableListOf() + val pluginTabs = mutableListOf() for (plugin in loadedPlugins) { try { @@ -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) } } @@ -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) diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt index 42ba8be549..8dfa39e24e 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt @@ -563,6 +563,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 diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt new file mode 100644 index 0000000000..fd0e65260c --- /dev/null +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt @@ -0,0 +1,68 @@ +package com.itsaky.androidide.plugins.manager.fragment + +import android.util.Log +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory + +class PluginFragmentFactory( + private val defaultFactory: FragmentFactory +) : FragmentFactory() { + + companion object { + private const val TAG = "PluginFragmentFactory" + + private val pluginClassLoaders = mutableMapOf() + + @JvmStatic + fun registerPluginClassLoader(pluginId: String, classLoader: ClassLoader, fragmentClassNames: List) { + fragmentClassNames.forEach { className -> + pluginClassLoaders[className] = classLoader + Log.d(TAG, "Registered classloader for fragment: $className (plugin: $pluginId)") + } + } + + @JvmStatic + fun unregisterPluginClassLoader(pluginId: String, fragmentClassNames: List) { + fragmentClassNames.forEach { className -> + pluginClassLoaders.remove(className) + Log.d(TAG, "Unregistered classloader for fragment: $className (plugin: $pluginId)") + } + } + + @JvmStatic + fun hasClassLoaderForFragment(className: String): Boolean { + return pluginClassLoaders.containsKey(className) + } + + @JvmStatic + fun getClassLoaderForFragment(className: String): ClassLoader? { + return pluginClassLoaders[className] + } + + @JvmStatic + fun clearAllClassLoaders() { + pluginClassLoaders.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) + } +} diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt index 800da13b78..74dec5be94 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment import com.itsaky.androidide.plugins.extensions.EditorTabExtension import com.itsaky.androidide.plugins.extensions.EditorTabItem import com.itsaky.androidide.plugins.manager.core.PluginManager +import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory import org.slf4j.LoggerFactory /** @@ -28,6 +29,7 @@ class PluginEditorTabManager { private val pluginTabs = mutableMapOf() private val tabFragments = mutableMapOf() private var tabSelectionListener: TabSelectionListener? = null + private var pluginManagerRef: PluginManager? = null interface TabSelectionListener { fun onTabSelected(tabId: String, fragment: Fragment) @@ -48,6 +50,7 @@ class PluginEditorTabManager { */ fun loadPluginTabs(pluginManager: PluginManager) { logger.debug("Loading plugin editor tabs...") + pluginManagerRef = pluginManager val loadedPlugins = pluginManager.getAllPluginInstances() logger.debug("Found {} loaded plugins", loadedPlugins.size) @@ -123,11 +126,35 @@ class PluginEditorTabManager { val fragment = tabInfo.tabItem.fragmentFactory() tabFragments[tabId] = fragment logger.debug("Created fragment for plugin tab: {}", tabId) + + registerFragmentClassLoader(tabInfo.extension, fragment) + fragment } } } + private fun registerFragmentClassLoader(extension: EditorTabExtension, fragment: Fragment) { + val pluginManager = pluginManagerRef ?: run { + logger.warn("PluginManager not available, cannot register fragment classloader") + return + } + + val classLoader = pluginManager.getClassLoaderForPlugin(extension) + if (classLoader != null) { + val fragmentClassName = fragment.javaClass.name + val pluginId = pluginManager.getPluginIdForInstance(extension) ?: "unknown" + PluginFragmentFactory.registerPluginClassLoader( + pluginId, + classLoader, + listOf(fragmentClassName) + ) + logger.info("Registered classloader for fragment {} from plugin {}", fragmentClassName, pluginId) + } else { + logger.warn("No classloader found for plugin extension: {}", extension.javaClass.name) + } + } + /** * Handle tab selection event. */ From 2a7b1aa6c59543200581d733c6134a58a1bd207d Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Mon, 19 Jan 2026 16:40:16 +0100 Subject: [PATCH 2/3] Fix thread safety and classloader leak in PluginFragmentFactory --- .../plugins/manager/core/PluginManager.kt | 4 ++++ .../manager/fragment/PluginFragmentFactory.kt | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt index 8dfa39e24e..c5f5198b82 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt @@ -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 @@ -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) { diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt index fd0e65260c..e74843d472 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/fragment/PluginFragmentFactory.kt @@ -3,6 +3,7 @@ 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 @@ -11,22 +12,39 @@ class PluginFragmentFactory( companion object { private const val TAG = "PluginFragmentFactory" - private val pluginClassLoaders = mutableMapOf() + private val pluginClassLoaders = ConcurrentHashMap() + private val pluginFragmentClasses = ConcurrentHashMap>() @JvmStatic fun registerPluginClassLoader(pluginId: String, classLoader: ClassLoader, fragmentClassNames: List) { + 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) { + 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)") } @JvmStatic @@ -42,6 +60,7 @@ class PluginFragmentFactory( @JvmStatic fun clearAllClassLoaders() { pluginClassLoaders.clear() + pluginFragmentClasses.clear() Log.d(TAG, "Cleared all plugin fragment classloaders") } } From 5feab846644bec48e7f10260851c16daafe75068 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Tue, 20 Jan 2026 12:35:11 +0100 Subject: [PATCH 3/3] Fix plugin tooltip display and documentation database initialization --- .../PluginDocumentationManager.kt | 12 ++++++++---- .../manager/tooltip/PluginTooltipManager.kt | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt index 8a3500d429..7c40bebf31 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt @@ -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) @@ -521,11 +521,15 @@ class PluginDocumentationManager(private val context: Context) { suspend fun verifyAllPluginDocumentation( plugins: Map ): 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) { diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt index 7f95c8bcbe..1b8df11d3e 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt @@ -11,6 +11,7 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.content.ContextWrapper import android.webkit.WebView import android.webkit.WebViewClient import android.widget.ImageButton @@ -200,14 +201,19 @@ object PluginTooltipManager { ) } - private fun canShowPopup(context: Context, view: View): Boolean { - val activityValid = (context as? Activity)?.let { - !it.isFinishing && !it.isDestroyed - } ?: false + private tailrec fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null + } - val viewAttached = view.isAttachedToWindow && view.windowToken != null + private fun canShowPopup(context: Context, view: View): Boolean { + if (!view.isAttachedToWindow || view.windowToken == null) { + return false + } - return activityValid && viewAttached + val activity = context.findActivity() ?: view.context.findActivity() + return activity == null || (!activity.isFinishing && !activity.isDestroyed) } /**