Skip to content

Instantly share code, notes, and snippets.

@ncipollo
Created October 29, 2021 17:42
Show Gist options
  • Save ncipollo/5d34ef0f533dbbad01fe6af932240bf6 to your computer and use it in GitHub Desktop.
Save ncipollo/5d34ef0f533dbbad01fe6af932240bf6 to your computer and use it in GitHub Desktop.
Paparazzi + Compose
// Mock Hacks
internal object ContextSwizzle {
fun swizzle() {
ByteBuddyAgent.install()
val bridgeContextClass = contextClass()
val buddy = ByteBuddy()
buddy.redefine(bridgeContextClass)
.method(
ElementMatchers.named<MethodDescription>("getSystemService")
.and(ElementMatchers.takesArguments(String::class.java))
)
.intercept(MethodDelegation.to(ContextMethodInterceptor::class.java))
.make()
.load(bridgeContextClass.classLoader, ClassReloadingStrategy.fromInstalledAgent())
}
fun unswizzle() {
val bridgeContextClass = contextClass()
ClassReloadingStrategy.fromInstalledAgent().reset(bridgeContextClass)
}
private fun contextClass() = Class.forName("com.android.layoutlib.bridge.android.BridgeContext")
}
internal object ContextMethodInterceptor {
@Suppress("unused") // Called via reflection
@JvmStatic
fun intercept(service: String, @This self: Any): Any {
return when (service) {
"layout_inflater" -> {
val fields = fieldMap(self)
fields["mBridgeInflater"]?.isAccessible = true
fields["mBridgeInflater"]!!.get(self)
}
"accessibility" -> mockClass("android.view.accessibility.AccessibilityManager") {
every { it["isEnabled"]() } returns false
}
"autofill" -> mockClass("android.view.autofill.AutofillManager") {
}
"clipboard" -> mockClass("android.content.ClipboardManager")
else -> error("No mock provided for service: $service")
}
}
private fun mockClass(className: String, block: (Any) -> Unit = {}) =
mockkClass(Class.forName(className).kotlin, relaxed = true).also(block)
private fun fieldMap(anyObject: Any): Map<String, Field> =
anyObject::class.java.declaredFields.map { it.name to it }.toMap()
}
// View Hacks
class SnapshotHostView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val parentView = parent as? View
parentView?.let {
val treeLifecycle = SimulatedLifecycle(state = Lifecycle.State.CREATED, simulateObserver = true)
ViewTreeLifecycleOwner.set(it) { treeLifecycle }
ViewTreeSavedStateRegistryOwner.set(it, SimulatedSavedStateOwner(SimulatedLifecycle()))
}
}
}
internal class SimulatedSavedStateOwner(private val lifecycle: Lifecycle) : SavedStateRegistryOwner {
override fun getLifecycle() = lifecycle
override fun getSavedStateRegistry(): SavedStateRegistry =
SavedStateRegistryController.create(this)
.apply { performRestore(null) }
.savedStateRegistry
}
internal class SimulatedLifecycle(
private val state: State = State.INITIALIZED,
private val simulateObserver: Boolean = false
) : Lifecycle() {
override fun addObserver(observer: LifecycleObserver) {
if (simulateObserver) {
val eventObserver = observer as? LifecycleEventObserver
eventObserver?.onStateChanged({ this }, Event.ON_CREATE)
}
}
override fun removeObserver(observer: LifecycleObserver) = Unit
override fun getCurrentState() = state
}
@timrijckaert
Copy link

timrijckaert commented Jan 12, 2022

How to use this exactly with Paparazzi?
And what dependencies did you use?

Related to
cashapp/paparazzi@e71a024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment