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
@@ -1,5 +1,7 @@
package com.itsaky.androidide.activities

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
Expand All @@ -11,103 +13,128 @@ import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
import com.itsaky.androidide.plugins.services.IdeUIService

class PluginScreenActivity : IDEActivity() {

private var pluginId: String? = null
private var fragmentClassName: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
pluginId = intent.getStringExtra(IdeUIService.EXTRA_PLUGIN_ID)
fragmentClassName = intent.getStringExtra(IdeUIService.EXTRA_FRAGMENT_CLASS_NAME)

val pluginId = pluginId
val fragmentClassName = fragmentClassName

if (pluginId.isNullOrBlank() || fragmentClassName.isNullOrBlank()) {
super.onCreate(savedInstanceState)
finish()
return
}

val classLoader = IDEApplication.getPluginManager()
?.getClassLoaderForPluginId(pluginId)

if (classLoader == null) {
super.onCreate(savedInstanceState)
finish()
return
}

registerPluginFragmentFactory(
pluginId = pluginId,
classLoader = classLoader,
fragmentClassName = fragmentClassName
)

super.onCreate(savedInstanceState)

if (savedInstanceState == null) {
loadPluginFragment(classLoader, fragmentClassName)
}
}

override fun bindLayout(): View {
return FrameLayout(this).apply {
id = R.id.plugin_screen_container
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
}

private fun registerPluginFragmentFactory(
pluginId: String,
classLoader: ClassLoader,
fragmentClassName: String
) {
supportFragmentManager.fragmentFactory =
PluginFragmentFactory(supportFragmentManager.fragmentFactory)

PluginFragmentFactory.registerPluginClassLoader(
pluginId,
classLoader,
listOf(fragmentClassName)
)
}

private fun loadPluginFragment(
classLoader: ClassLoader,
fragmentClassName: String
) {
val fragment = runCatching {
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
fragmentClassName
)
}.getOrElse { error ->
Log.e(TAG, "Failed to instantiate plugin fragment: $fragmentClassName", error)
finish()
return
}

supportFragmentManager.beginTransaction()
.replace(R.id.plugin_screen_container, fragment, TAG_PLUGIN_SCREEN)
.commit()
}

override fun onDestroy() {
val pluginId = pluginId
val fragmentClassName = fragmentClassName

if (!pluginId.isNullOrBlank() && !fragmentClassName.isNullOrBlank()) {
PluginFragmentFactory.unregisterPluginClassLoader(pluginId, listOf(fragmentClassName))
}

super.onDestroy()
}

companion object {
private const val TAG = "PluginScreenActivity"
private const val TAG_PLUGIN_SCREEN = "plugin_screen"
}
private var pluginId: String? = null
private var fragmentClassName: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
pluginId = intent.getStringExtra(IdeUIService.EXTRA_PLUGIN_ID)
fragmentClassName = intent.getStringExtra(IdeUIService.EXTRA_FRAGMENT_CLASS_NAME)

val pluginId = pluginId
val fragmentClassName = fragmentClassName

if (pluginId.isNullOrBlank() || fragmentClassName.isNullOrBlank()) {
super.onCreate(savedInstanceState)
finish()
return
}

val classLoader =
IDEApplication
.getPluginManager()
?.getClassLoaderForPluginId(pluginId)

if (classLoader == null) {
super.onCreate(savedInstanceState)
finish()
return
}

registerPluginFragmentFactory(
pluginId = pluginId,
classLoader = classLoader,
fragmentClassName = fragmentClassName,
)

super.onCreate(savedInstanceState)

if (savedInstanceState == null) {
loadPluginFragment(classLoader, fragmentClassName)
}
}

override fun bindLayout(): View =
FrameLayout(this).apply {
id = R.id.plugin_screen_container
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
)
}

private fun registerPluginFragmentFactory(
pluginId: String,
classLoader: ClassLoader,
fragmentClassName: String,
) {
supportFragmentManager.fragmentFactory =
PluginFragmentFactory(supportFragmentManager.fragmentFactory)

PluginFragmentFactory.registerPluginClassLoader(
pluginId,
classLoader,
listOf(fragmentClassName),
)
}

private fun loadPluginFragment(
classLoader: ClassLoader,
fragmentClassName: String,
) {
val fragment =
runCatching {
supportFragmentManager.fragmentFactory.instantiate(
classLoader,
fragmentClassName,
)
}.getOrElse { error ->
Log.e(TAG, "Failed to instantiate plugin fragment: $fragmentClassName", error)
finish()
return
}

supportFragmentManager
.beginTransaction()
.replace(R.id.plugin_screen_container, fragment, TAG_PLUGIN_SCREEN)
.commit()
}

override fun onDestroy() {
val pluginId = pluginId
val fragmentClassName = fragmentClassName

if (!pluginId.isNullOrBlank() && !fragmentClassName.isNullOrBlank()) {
PluginFragmentFactory.unregisterPluginClassLoader(pluginId, listOf(fragmentClassName))
}

super.onDestroy()
}

companion object {
private const val TAG = "PluginScreenActivity"
private const val TAG_PLUGIN_SCREEN = "plugin_screen"

/**
* The intent that mounts [fragmentClassName] of [pluginId] on this screen - the same one
* `IdeUIServiceImpl.openPluginScreen` builds, so both entry points stay in step. Action-based
* rather than explicit-component to match the manifest intent-filter; [setPackage] keeps it
* inside the app.
*
* No reuse flags: every entry is its own screen, and the extras are only read in [onCreate],
* so a reused instance would keep showing the previous plugin's fragment.
*/
fun newIntent(
context: Context,
pluginId: String,
fragmentClassName: String,
title: String? = null,
): Intent =
Intent(IdeUIService.ACTION_OPEN_PLUGIN_SCREEN).apply {
setPackage(context.packageName)
putExtra(IdeUIService.EXTRA_PLUGIN_ID, pluginId)
putExtra(IdeUIService.EXTRA_FRAGMENT_CLASS_NAME, fragmentClassName)
putExtra(IdeUIService.EXTRA_TITLE, title)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.itsaky.androidide.activities

import android.content.Context
import org.slf4j.LoggerFactory

/**
* Launches a plugin-owned screen in [PluginScreenActivity].
*
* Kept out of the callers so that "how a plugin screen is opened" lives in one place: today the
* contributed preference rows, later any other surface that wants to mount plugin UI. Callers decide
* what the user sees when it fails - the message belongs to the surface, not to the launch.
*/
object PluginScreenNavigator {
private val log = LoggerFactory.getLogger("PluginScreenNavigator")

/**
* Opens [fragmentClassName] of the plugin [pluginId], full-screen.
*
* @return true when the screen was launched; false (already logged) when it could not be, so the
* caller can tell the user.
*/
fun openPluginScreen(
context: Context,
pluginId: String,
fragmentClassName: String,
title: String? = null,
): Boolean =
try {
context.startActivity(
PluginScreenActivity.newIntent(context, pluginId, fragmentClassName, title),
)
true
} catch (error: Exception) {
log.error("Failed to open screen {} of plugin {}", fragmentClassName, pluginId, error)
false
}
}
Loading
Loading