Skip to content
Open
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,6 +1,7 @@
package com.itsaky.androidide.plugins.base

import android.content.Context
import android.content.ContextWrapper
import android.content.res.Resources
import android.view.LayoutInflater
import com.itsaky.androidide.plugins.ServiceRegistry
Expand All @@ -15,111 +16,120 @@ import java.lang.ref.WeakReference
*/
object PluginFragmentHelper {

@JvmStatic
var onPluginInflationError: ((pluginId: String, error: Throwable) -> Unit)? = null

private val pluginContexts = mutableMapOf<String, Context>()
private val serviceRegistries = mutableMapOf<String, ServiceRegistry>()
private val legacyPlugins = mutableSetOf<String>()

private data class ActivitySnapshot(
val contextRef: WeakReference<Context>,
val themeRef: WeakReference<Resources.Theme>
)

private val activitySnapshots = mutableMapOf<String, ActivitySnapshot>()

@JvmStatic
fun getCurrentActivityTheme(pluginId: String): Resources.Theme? =
activitySnapshots[pluginId]?.themeRef?.get()

@JvmStatic
fun getCurrentActivityContext(pluginId: String): Context? =
activitySnapshots[pluginId]?.contextRef?.get()

/**
* Register a plugin's resource context.
* Called by the plugin manager when loading APK-based plugins.
*/
@InternalPluginApi
@JvmStatic
@JvmOverloads
fun registerPluginContext(pluginId: String, context: Context, isLegacy: Boolean = false) {
pluginContexts[pluginId] = context
if (isLegacy) legacyPlugins.add(pluginId) else legacyPlugins.remove(pluginId)
}

/**
* Unregister a plugin's resource context.
* Called when a plugin is unloaded.
*/
@InternalPluginApi
@JvmStatic
fun unregisterPluginContext(pluginId: String) {
pluginContexts.remove(pluginId)
activitySnapshots.remove(pluginId)
legacyPlugins.remove(pluginId)
}

/**
* Get the resource context for a specific plugin.
*/
@JvmStatic
fun getPluginContext(pluginId: String): Context? {
return pluginContexts[pluginId]
}

/**
* Register a plugin's service registry.
* Called by the plugin manager when creating plugin context.
*/
@InternalPluginApi
@JvmStatic
fun registerServiceRegistry(pluginId: String, registry: ServiceRegistry) {
serviceRegistries[pluginId] = registry
}

/**
* Get the service registry for a specific plugin.
*/
@JvmStatic
fun getServiceRegistry(pluginId: String): ServiceRegistry? {
return serviceRegistries[pluginId]
}

/**
* Clear all registered plugin contexts.
*/
@InternalPluginApi
@JvmStatic
fun clearAllContexts() {
pluginContexts.clear()
serviceRegistries.clear()
activitySnapshots.clear()
legacyPlugins.clear()
}

/**
* Create a LayoutInflater with the plugin's resource context.
* Fragments should call this in their onGetLayoutInflater() method.
*
* @param pluginId The plugin ID
* @param defaultInflater The default inflater from the fragment
* @return An inflater with the plugin's context, or the default if not found
*/
@JvmStatic
fun getPluginInflater(pluginId: String, defaultInflater: LayoutInflater): LayoutInflater {
val pluginContext = getPluginContext(pluginId) ?: return defaultInflater
activitySnapshots[pluginId] = ActivitySnapshot(
contextRef = WeakReference(defaultInflater.context),
themeRef = WeakReference(defaultInflater.context.theme)
)
val inflater = if (pluginId in legacyPlugins) {
pluginContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
?: defaultInflater.cloneInContext(pluginContext)
} else {
defaultInflater.cloneInContext(pluginContext)
}
return SafePluginLayoutInflater.wrap(inflater, pluginId)
}
}
@JvmStatic
var onPluginInflationError: ((pluginId: String, error: Throwable) -> Unit)? = null

private val pluginContexts = mutableMapOf<String, Context>()
private val serviceRegistries = mutableMapOf<String, ServiceRegistry>()
private val legacyPlugins = mutableSetOf<String>()

private data class ActivitySnapshot(
val contextRef: WeakReference<Context>,
val themeRef: WeakReference<Resources.Theme>
)

private val activitySnapshots = mutableMapOf<String, ActivitySnapshot>()

@JvmStatic
fun getCurrentActivityTheme(pluginId: String): Resources.Theme? =
activitySnapshots[pluginId]?.themeRef?.get()

@JvmStatic
fun getCurrentActivityContext(pluginId: String): Context? =
activitySnapshots[pluginId]?.contextRef?.get()

/**
* Register a plugin's resource context.
* Called by the plugin manager when loading APK-based plugins.
*/
@InternalPluginApi
@JvmStatic
@JvmOverloads
fun registerPluginContext(pluginId: String, context: Context, isLegacy: Boolean = false) {
pluginContexts[pluginId] = context
if (isLegacy) legacyPlugins.add(pluginId) else legacyPlugins.remove(pluginId)
}

/**
* Unregister a plugin's resource context.
* Called when a plugin is unloaded.
*/
@InternalPluginApi
@JvmStatic
fun unregisterPluginContext(pluginId: String) {
pluginContexts.remove(pluginId)
activitySnapshots.remove(pluginId)
legacyPlugins.remove(pluginId)
}

/**
* Get the resource context for a specific plugin.
*/
@JvmStatic
fun getPluginContext(pluginId: String): Context? {
return pluginContexts[pluginId]
}

/**
* Register a plugin's service registry.
* Called by the plugin manager when creating plugin context.
*/
@InternalPluginApi
@JvmStatic
fun registerServiceRegistry(pluginId: String, registry: ServiceRegistry) {
serviceRegistries[pluginId] = registry
}

/**
* Get the service registry for a specific plugin.
*/
@JvmStatic
fun getServiceRegistry(pluginId: String): ServiceRegistry? {
return serviceRegistries[pluginId]
}

/**
* Clear all registered plugin contexts.
*/
@InternalPluginApi
@JvmStatic
fun clearAllContexts() {
pluginContexts.clear()
serviceRegistries.clear()
activitySnapshots.clear()
legacyPlugins.clear()
}

/**
* Create a LayoutInflater with the plugin's resource context.
* Fragments should call this in their onGetLayoutInflater() method.
*
* @param pluginId The plugin ID
* @param defaultInflater The default inflater from the fragment
* @return An inflater with the plugin's context, or the default if not found
*/
@JvmStatic
fun getPluginInflater(pluginId: String, defaultInflater: LayoutInflater): LayoutInflater {
val pluginContext = getPluginContext(pluginId) ?: return defaultInflater
val hostContext = defaultInflater.context
if (!hostContext.isOrWraps(pluginContext)) {
activitySnapshots[pluginId] = ActivitySnapshot(
contextRef = WeakReference(hostContext),
themeRef = WeakReference(hostContext.theme)
)
}
val inflater = if (pluginId in legacyPlugins) {
pluginContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
?: defaultInflater.cloneInContext(pluginContext)
} else {
defaultInflater.cloneInContext(pluginContext)
}
return SafePluginLayoutInflater.wrap(inflater, pluginId)
}
}

private tailrec fun Context.isOrWraps(target: Context): Boolean {
if (this === target) return true
val base = (this as? ContextWrapper)?.baseContext ?: return false
return base.isOrWraps(target)
}
1 change: 1 addition & 0 deletions plugin-manager/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ dependencies {

testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.robolectric)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.itsaky.androidide.plugins.manager.loaders.fakeplugin;

public final class R {
public static final class drawable {
public static final int icon = 0x57020000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.itsaky.androidide.plugins.manager.loaders

import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.view.LayoutInflater
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.plugins.base.PluginFragmentHelper
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class PluginResourceContextActivitySnapshotTest {
private val app: Context = RuntimeEnvironment.getApplication()

private val pluginContext =
PluginResourceContext(
app,
PLUGIN_ID,
app.resources,
PackageInfo().apply {
packageName = "com.itsaky.androidide.plugins.manager.loaders.fakeplugin"
applicationInfo = ApplicationInfo()
},
javaClass.classLoader,
)

@Before
fun registerPlugin() {
assertThat(pluginContext.usesCustomPackageId()).isTrue()
PluginFragmentHelper.registerPluginContext(PLUGIN_ID, pluginContext)
PluginFragmentHelper.getPluginInflater(PLUGIN_ID, LayoutInflater.from(app))
}

@After
fun unregisterPlugin() {
PluginFragmentHelper.unregisterPluginContext(PLUGIN_ID)
}

@Test
fun anInflaterInThePluginContextDoesNotReplaceTheActivitySnapshot() {
PluginFragmentHelper.getPluginInflater(PLUGIN_ID, LayoutInflater.from(pluginContext))

assertThat(PluginFragmentHelper.getCurrentActivityContext(PLUGIN_ID)).isSameInstanceAs(app)
assertThat(pluginContext.resources).isSameInstanceAs(app.resources)
}

@Test
fun anInflaterOverAWrapperOfThePluginContextDoesNotReplaceTheActivitySnapshot() {
val wrapped = LayoutInflater.from(pluginContext).cloneInContext(ContextWrapper(pluginContext))
PluginFragmentHelper.getPluginInflater(PLUGIN_ID, wrapped)

assertThat(PluginFragmentHelper.getCurrentActivityContext(PLUGIN_ID)).isSameInstanceAs(app)
assertThat(pluginContext.resources).isSameInstanceAs(app.resources)
}

private companion object {
const val PLUGIN_ID = "com.itsaky.androidide.plugins.manager.loaders.fakeplugin"
}
}
Loading