From 0900a2a41d703eb077ea597b98027a46a8854333 Mon Sep 17 00:00:00 2001 From: Pascal Hartig Date: Fri, 8 Sep 2023 04:02:48 -0700 Subject: [PATCH] Export android plugin to OSS (#5109) Summary: Changelog: UI Debugger is now available for Litho in Open Source Pull Request resolved: https://github.com/facebook/flipper/pull/5109 Remove the stub, replace with the real thing. Reviewed By: lblasa Differential Revision: D46859213 fbshipit-source-id: 74c59a53d1d22e046254f4bca202da17a0b0e5d8 --- .../litho/UIDebuggerLithoSupport.kt | 124 +++++ .../litho/UIDebuggerLithoSupportStub.kt | 16 - .../descriptors/ComponentTreeDescriptor.kt | 63 +++ .../descriptors/DebugComponentDescriptor.kt | 188 ++++++++ .../litho/descriptors/LithoViewDescriptor.kt | 48 ++ .../descriptors/MatrixDrawableDescriptor.kt | 25 + .../descriptors/TextDrawableDescriptor.kt | 38 ++ .../props/ComponentDataExtractor.kt | 187 ++++++++ .../descriptors/props/LayoutPropExtractor.kt | 432 ++++++++++++++++++ 9 files changed, 1105 insertions(+), 16 deletions(-) create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt delete mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupportStub.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt create mode 100644 android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt new file mode 100644 index 000000000..c1c389db2 --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupport.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho + +import com.facebook.flipper.plugins.uidebugger.core.ConnectionListener +import com.facebook.flipper.plugins.uidebugger.core.UIDContext +import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister +import com.facebook.flipper.plugins.uidebugger.litho.descriptors.* +import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent +import com.facebook.flipper.plugins.uidebugger.model.FrameworkEventMetadata +import com.facebook.litho.ComponentTree +import com.facebook.litho.DebugComponent +import com.facebook.litho.LithoView +import com.facebook.litho.MatrixDrawable +import com.facebook.litho.debug.LithoDebugEvent +import com.facebook.litho.widget.TextDrawable +import com.facebook.rendercore.debug.DebugEvent +import com.facebook.rendercore.debug.DebugEventAttribute +import com.facebook.rendercore.debug.DebugEventBus +import com.facebook.rendercore.debug.DebugEventSubscriber +import com.facebook.rendercore.debug.DebugMarkerEvent +import com.facebook.rendercore.debug.DebugProcessEvent +import com.facebook.rendercore.debug.Duration + +const val LithoTag = "Litho" +const val LithoMountableTag = "LithoMountable" + +object UIDebuggerLithoSupport { + + fun enable(context: UIDContext) { + addDescriptors(context.descriptorRegister) + + val eventMeta = + listOf( + // Litho + FrameworkEventMetadata( + LithoDebugEvent.StateUpdateEnqueued, + "Set state was called, this will trigger resolve and then possibly layout and mount"), + FrameworkEventMetadata( + LithoDebugEvent.RenderRequest, + "A request to render the component tree again. It can be requested due to 1) set root 2) state update 3) size change or measurement"), + FrameworkEventMetadata( + LithoDebugEvent.ComponentTreeResolve, + "ComponentTree resolved the hierarchy into a LayoutState, non layout nodes are removed, see attributes for source of execution"), + FrameworkEventMetadata( + LithoDebugEvent.LayoutCommitted, + "A new layout state created (resolved and measured result) being committed; this layout state could get mounted next."), + + // RenderCore + + FrameworkEventMetadata( + DebugEvent.RenderTreeMounted, "The mount phase for the entire render tree"), + FrameworkEventMetadata( + DebugEvent.RenderUnitMounted, + "Component was added into the view hierarchy (this doesn't mean it is visible)"), + FrameworkEventMetadata( + DebugEvent.RenderUnitUpdated, + "The properties of a component's content were were rebinded"), + FrameworkEventMetadata( + DebugEvent.RenderUnitUnmounted, "Component was removed from the view hierarchy"), + FrameworkEventMetadata(DebugEvent.RenderUnitOnVisible, "Component became visible"), + FrameworkEventMetadata(DebugEvent.RenderUnitOnInvisible, "Component became invisible"), + ) + + val eventForwarder = + object : DebugEventSubscriber(*eventMeta.map { it.type }.toTypedArray()) { + override fun onEvent(event: DebugEvent) { + val timestamp = + when (event) { + is DebugMarkerEvent -> event.timestamp + is DebugProcessEvent -> event.timestamp + } + val treeId = event.renderStateId.toIntOrNull() ?: -1 + + val globalKey = + event.attributeOrNull(DebugEventAttribute.GlobalKey)?.let { + DebugComponent.generateGlobalKey(treeId, it).hashCode() + } + val duration = event.attributeOrNull(DebugEventAttribute.duration) + + val attributes = mutableMapOf() + val source = event.attributeOrNull(DebugEventAttribute.source) + if (source != null) { + attributes["source"] = source + } + context.addFrameworkEvent( + FrameworkEvent( + treeId, + globalKey ?: treeId, + event.type, + timestamp, + duration?.value, + event.threadName, + attributes)) + } + } + + context.connectionListeners.add( + object : ConnectionListener { + override fun onConnect() { + DebugEventBus.subscribe(eventForwarder) + } + + override fun onDisconnect() { + DebugEventBus.unsubscribe(eventForwarder) + } + }) + + context.frameworkEventMetadata.addAll(eventMeta) + } + + private fun addDescriptors(register: DescriptorRegister) { + register.register(LithoView::class.java, LithoViewDescriptor) + register.register(DebugComponent::class.java, DebugComponentDescriptor(register)) + register.register(TextDrawable::class.java, TextDrawableDescriptor) + register.register(MatrixDrawable::class.java, MatrixDrawableDescriptor) + register.register(ComponentTree::class.java, ComponentTreeDescriptor(register)) + } +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupportStub.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupportStub.kt deleted file mode 100644 index a6241e0f9..000000000 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/UIDebuggerLithoSupportStub.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.flipper.plugins.uidebugger.litho - -import com.facebook.flipper.plugins.uidebugger.core.UIDContext - -// this is not used internally -object UIDebuggerLithoSupport { - - fun enable(context: UIDContext) {} -} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt new file mode 100644 index 000000000..22bd068a1 --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/ComponentTreeDescriptor.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors + +import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister +import com.facebook.flipper.plugins.uidebugger.descriptors.Id +import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor +import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild +import com.facebook.flipper.plugins.uidebugger.litho.LithoTag +import com.facebook.flipper.plugins.uidebugger.model.Bounds +import com.facebook.flipper.plugins.uidebugger.model.InspectableObject +import com.facebook.flipper.plugins.uidebugger.model.MetadataId +import com.facebook.flipper.plugins.uidebugger.util.Immediate +import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred +import com.facebook.litho.ComponentTree +import com.facebook.litho.DebugComponent + +class ComponentTreeDescriptor(val register: DescriptorRegister) : NodeDescriptor { + + private val qualifiedName = ComponentTree::class.qualifiedName ?: "" + + override fun getId(node: ComponentTree): Id = node.id + + override fun getBounds(node: ComponentTree): Bounds { + val rootComponent = DebugComponent.getRootInstance(node) + return if (rootComponent != null) { + Bounds.fromRect(rootComponent.boundsInParentDebugComponent) + } else { + Bounds(0, 0, 0, 0) + } + } + + override fun getName(node: ComponentTree): String = "ComponentTree" + + override fun getQualifiedName(node: ComponentTree): String = qualifiedName + + override fun getChildren(node: ComponentTree): List { + val result = mutableListOf() + val debugComponent = DebugComponent.getRootInstance(node) + if (debugComponent != null) { + result.add( + // we want the component tree to take the size and any offset so we reset this one + OffsetChild.zero( + debugComponent, register.descriptorForClassUnsafe(debugComponent.javaClass))) + } + return result + } + + override fun getActiveChild(node: ComponentTree): Any? = null + + override fun getAttributes( + node: ComponentTree + ): MaybeDeferred> { + return Immediate(mapOf()) + } + + override fun getTags(node: ComponentTree): Set = setOf(LithoTag, "TreeRoot") +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt new file mode 100644 index 000000000..6de5e232d --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors + +import android.graphics.Bitmap +import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister +import com.facebook.flipper.plugins.uidebugger.descriptors.Id +import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister +import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor +import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild +import com.facebook.flipper.plugins.uidebugger.litho.LithoMountableTag +import com.facebook.flipper.plugins.uidebugger.litho.LithoTag +import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.ComponentDataExtractor +import com.facebook.flipper.plugins.uidebugger.litho.descriptors.props.LayoutPropExtractor +import com.facebook.flipper.plugins.uidebugger.model.Bounds +import com.facebook.flipper.plugins.uidebugger.model.Inspectable +import com.facebook.flipper.plugins.uidebugger.model.InspectableObject +import com.facebook.flipper.plugins.uidebugger.model.InspectableValue +import com.facebook.flipper.plugins.uidebugger.model.MetadataId +import com.facebook.flipper.plugins.uidebugger.util.Deferred +import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred +import com.facebook.litho.Component +import com.facebook.litho.DebugComponent +import com.facebook.rendercore.FastMath +import com.facebook.yoga.YogaEdge + +class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor { + private val NAMESPACE = "DebugComponent" + + /* + * Debug component is generated on the fly so use the underlying component instance which is + * immutable + */ + override fun getId(node: DebugComponent): Id = node.globalKey.hashCode() + + override fun getName(node: DebugComponent): String = node.component.simpleName + + override fun getQualifiedName(node: com.facebook.litho.DebugComponent): String = + node.component::class.qualifiedName ?: "" + + override fun getChildren(node: DebugComponent): List { + val result = mutableListOf() + + val mountedContent = node.mountedContent + + if (mountedContent == null) { + for (child in node.childComponents) { + result.add(child) + } + } else { + + val layoutNode = node.layoutNode + val descriptor: NodeDescriptor = + register.descriptorForClassUnsafe(mountedContent.javaClass) + // mountables are always layout nodes + if (layoutNode != null) { + /** + * We need to override the mounted contents offset since the mounted contents android bounds + * are w.r.t its native parent but we want it w.r.t to the mountable. + * + * However padding on a mountable means that the content is inset within the mountables + * bounds so we need to adjust for this + */ + result.add( + OffsetChild( + child = mountedContent, + descriptor = descriptor, + x = layoutNode.getLayoutPadding(YogaEdge.LEFT).let { FastMath.round(it) }, + y = layoutNode.getLayoutPadding(YogaEdge.TOP).let { FastMath.round(it) }, + )) + } + } + + return result + } + + override fun getActiveChild(node: DebugComponent): Any? = null + + private val LayoutId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Layout") + + private val UserPropsId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho Props") + + private val StateId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho State") + + private val MountingDataId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Mount State") + + private val isMountedAttributeId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "mounted") + + private val isVisibleAttributeId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "visible") + + override fun getAttributes( + node: DebugComponent + ): MaybeDeferred> { + return Deferred { + val attributeSections = mutableMapOf() + + val mountingData = getMountingData(node) + attributeSections[MountingDataId] = InspectableObject(mountingData) + + val layoutProps = LayoutPropExtractor.getProps(node) + attributeSections[LayoutId] = InspectableObject(layoutProps.toMap()) + + if (!node.canResolve()) { + val stateContainer = node.stateContainer + if (stateContainer != null) { + attributeSections[StateId] = + ComponentDataExtractor.getState(stateContainer, node.component.simpleName) + } + + val props = ComponentDataExtractor.getProps(node.component) + + attributeSections[UserPropsId] = InspectableObject(props.toMap()) + } + + attributeSections + } + } + + override fun getBounds(node: DebugComponent): Bounds = + Bounds.fromRect(node.boundsInParentDebugComponent) + + override fun getTags(node: DebugComponent): Set { + val tags = mutableSetOf(LithoTag) + + if (node.component.mountType != Component.MountType.NONE) { + tags.add(LithoMountableTag) + } + return tags + } + + override fun getSnapshot(node: DebugComponent, bitmap: Bitmap?): Bitmap? = null + + override fun getInlineAttributes(node: DebugComponent): Map { + val attributes = mutableMapOf() + val key = node.key + val testKey = node.testKey + if (key != null && key.trim { it <= ' ' }.length > 0) { + attributes["key"] = key + } + if (testKey != null && testKey.trim { it <= ' ' }.length > 0) { + attributes["testKey"] = testKey + } + return attributes + } + + private fun getMountingData(node: DebugComponent): Map { + + val lithoView = node.lithoView + val mountingData = mutableMapOf() + + if (lithoView == null) { + return mountingData + } + + val mountState = lithoView.mountDelegateTarget ?: return mountingData + val componentTree = lithoView.componentTree ?: return mountingData + + val component = node.component + + if (component.mountType != Component.MountType.NONE) { + val renderUnit = DebugComponent.getRenderUnit(node, componentTree) + if (renderUnit != null) { + val renderUnitId = renderUnit.id + val isMounted = mountState.getContentById(renderUnitId) != null + mountingData[isMountedAttributeId] = InspectableValue.Boolean(isMounted) + } + } + + val visibilityOutput = DebugComponent.getVisibilityOutput(node, componentTree) + if (visibilityOutput != null) { + val isVisible = DebugComponent.isVisible(node, lithoView) + mountingData[isVisibleAttributeId] = InspectableValue.Boolean(isVisible) + } + + return mountingData + } +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt new file mode 100644 index 000000000..26ffe7b36 --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors + +import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor +import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister +import com.facebook.flipper.plugins.uidebugger.model.InspectableObject +import com.facebook.flipper.plugins.uidebugger.model.InspectableValue +import com.facebook.flipper.plugins.uidebugger.model.MetadataId +import com.facebook.litho.LithoView + +object LithoViewDescriptor : ChainedDescriptor() { + + private const val NAMESPACE = "LithoView" + private val SectionId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE) + + override fun onGetName(node: LithoView): String = node.javaClass.simpleName + + override fun onGetChildren(node: LithoView): List { + val componentTree = node.componentTree + if (componentTree != null) { + return listOf(componentTree) + } + + return listOf() + } + + private val IsIncrementalMountEnabledAttributeId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isIncrementalMountEnabled") + + override fun onGetAttributes( + node: LithoView, + attributeSections: MutableMap + ) { + attributeSections[SectionId] = + InspectableObject( + mapOf( + IsIncrementalMountEnabledAttributeId to + InspectableValue.Boolean(node.isIncrementalMountEnabled))) + } +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt new file mode 100644 index 000000000..d8f9117ab --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/MatrixDrawableDescriptor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors + +import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor +import com.facebook.litho.MatrixDrawable + +object MatrixDrawableDescriptor : ChainedDescriptor>() { + + override fun onGetChildren(node: MatrixDrawable<*>): List? { + val mountedDrawable = node.mountedDrawable + return if (mountedDrawable != null) { + listOf(mountedDrawable) + } else { + listOf() + } + } + + override fun onGetName(node: MatrixDrawable<*>): String = node.javaClass.simpleName +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt new file mode 100644 index 000000000..0ec132b6e --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors + +import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor +import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister +import com.facebook.flipper.plugins.uidebugger.model.Inspectable +import com.facebook.flipper.plugins.uidebugger.model.InspectableObject +import com.facebook.flipper.plugins.uidebugger.model.InspectableValue +import com.facebook.flipper.plugins.uidebugger.model.MetadataId +import com.facebook.litho.widget.TextDrawable + +object TextDrawableDescriptor : ChainedDescriptor() { + + private const val NAMESPACE = "TextDrawable" + private val SectionId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE) + + override fun onGetName(node: TextDrawable): String = node.javaClass.simpleName + + private val TextAttributeId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "text") + + override fun onGetAttributes( + node: TextDrawable, + attributeSections: MutableMap + ) { + val props = + mapOf(TextAttributeId to InspectableValue.Text(node.text.toString())) + + attributeSections[SectionId] = InspectableObject(props) + } +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt new file mode 100644 index 000000000..a07286d0e --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/ComponentDataExtractor.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors.props + +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import com.facebook.flipper.plugins.uidebugger.LogTag +import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister +import com.facebook.flipper.plugins.uidebugger.model.* +import com.facebook.litho.Component +import com.facebook.litho.SpecGeneratedComponent +import com.facebook.litho.StateContainer +import com.facebook.litho.annotations.Prop +import com.facebook.litho.annotations.ResType +import com.facebook.litho.annotations.State +import com.facebook.litho.editor.EditorRegistry +import com.facebook.litho.editor.model.EditorArray +import com.facebook.litho.editor.model.EditorBool +import com.facebook.litho.editor.model.EditorColor +import com.facebook.litho.editor.model.EditorNumber +import com.facebook.litho.editor.model.EditorPick +import com.facebook.litho.editor.model.EditorShape +import com.facebook.litho.editor.model.EditorString +import com.facebook.litho.editor.model.EditorValue +import com.facebook.litho.editor.model.EditorValue.EditorVisitor + +object ComponentDataExtractor { + + fun getProps(component: Component): Map { + val props = mutableMapOf() + + val isSpecComponent = component is SpecGeneratedComponent + + for (declaredField in component.javaClass.declaredFields) { + declaredField.isAccessible = true + + val name = declaredField.name + val declaredFieldAnnotation = declaredField.getAnnotation(Prop::class.java) + + // Only expose `@Prop` annotated fields for Spec components + if (isSpecComponent && declaredFieldAnnotation == null) { + continue + } + + val prop = + try { + declaredField[component] + } catch (e: IllegalAccessException) { + continue + } + + if (declaredFieldAnnotation != null) { + val resType = declaredFieldAnnotation.resType + if (resType == ResType.COLOR) { + if (prop != null) { + val identifier = getMetadataId(component.simpleName, name) + props[identifier] = InspectableValue.Color(Color.fromColor(prop as Int)) + } + continue + } else if (resType == ResType.DRAWABLE) { + val identifier = getMetadataId(component.simpleName, name) + props[identifier] = fromDrawable(prop as Drawable?) + continue + } + } + + val editorValue = + try { + EditorRegistry.read(declaredField.type, declaredField, component) + } catch (e: Exception) { + Log.d( + LogTag, + "Unable to retrieve prop ${declaredField.name} on type ${component.simpleName}") + EditorString("error fetching prop") + } + + if (editorValue != null) { + addProp(props, component.simpleName, name, editorValue) + } + } + + return props + } + + fun getState(stateContainer: StateContainer, componentName: String): InspectableObject { + + val stateFields = mutableMapOf() + for (field in stateContainer.javaClass.declaredFields) { + field.isAccessible = true + val stateAnnotation = field.getAnnotation(State::class.java) + val isKStateField = field.name == "states" + if (stateAnnotation != null || isKStateField) { + val id = getMetadataId(componentName, field.name) + val editorValue: EditorValue? = EditorRegistry.read(field.type, field, stateContainer) + if (editorValue != null) { + stateFields[id] = toInspectable(field.name, editorValue) + } + } + } + return InspectableObject(stateFields) + } + + private fun getMetadataId( + namespace: String, + key: String, + mutable: Boolean = false, + possibleValues: Set? = emptySet() + ): MetadataId { + val metadata = MetadataRegister.get(namespace, key) + val identifier = + metadata?.id + ?: MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, namespace, key, mutable, possibleValues) + return identifier + } + + private fun addProp( + props: MutableMap, + namespace: String, + name: String, + value: EditorValue + ) { + var possibleValues: MutableSet? = null + if (value is EditorPick) { + possibleValues = mutableSetOf() + value.values.forEach { possibleValues.add(InspectableValue.Text(it)) } + } + + val identifier = getMetadataId(namespace, name, false, possibleValues) + props[identifier] = toInspectable(name, value) + } + + private fun toInspectable(name: String, editorValue: EditorValue): Inspectable { + return editorValue.`when`( + object : EditorVisitor { + override fun isShape(shape: EditorShape): Inspectable { + + val fields = mutableMapOf() + shape.value.entries.forEach { entry -> + val value = toInspectable(entry.key, entry.value) + + val shapeEditorValue = entry.value + var possibleValues: MutableSet? = null + if (shapeEditorValue is EditorPick) { + possibleValues = mutableSetOf() + shapeEditorValue.values.forEach { possibleValues.add(InspectableValue.Text(it)) } + } + + val identifier = getMetadataId(name, entry.key, false, possibleValues) + fields[identifier] = value + } + + return InspectableObject(fields) + } + + override fun isArray(array: EditorArray?): Inspectable { + val values = array?.value?.map { value -> toInspectable(name, value) } + return InspectableArray(values ?: listOf()) + } + + override fun isPick(pick: EditorPick): Inspectable = InspectableValue.Enum(pick.selected) + + override fun isNumber(number: EditorNumber): Inspectable = + InspectableValue.Number(number.value) + + override fun isColor(number: EditorColor): Inspectable = + InspectableValue.Color(number.value.toInt().let { Color.fromColor(it) }) + + override fun isString(string: EditorString): Inspectable = + InspectableValue.Text(string.value ?: "") + + override fun isBool(bool: EditorBool): Inspectable = InspectableValue.Boolean(bool.value) + }) + } + + private fun fromDrawable(d: Drawable?): Inspectable = + when (d) { + is ColorDrawable -> InspectableValue.Color(Color.fromColor(d.color)) + else -> InspectableValue.Unknown(d.toString()) + } +} diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt new file mode 100644 index 000000000..a2f2ef651 --- /dev/null +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/props/LayoutPropExtractor.kt @@ -0,0 +1,432 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.plugins.uidebugger.litho.descriptors.props + +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import com.facebook.flipper.plugins.uidebugger.common.enumToInspectableSet +import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister +import com.facebook.flipper.plugins.uidebugger.model.* +import com.facebook.litho.DebugComponent +import com.facebook.yoga.* + +object LayoutPropExtractor { + private const val NAMESPACE = "LayoutPropExtractor" + + private var BackgroundId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "background") + private var ForegroundId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "foreground") + + private val DirectionId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "direction", + false, + enumToInspectableSet()) + private val FlexDirectionId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "flexDirection", + false, + enumToInspectableSet()) + private val JustifyContentId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "justifyContent", + false, + enumToInspectableSet()) + private val AlignItemsId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "alignItems", + false, + enumToInspectableSet()) + private val AlignSelfId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "alignSelf", + false, + enumToInspectableSet()) + private val AlignContentId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "alignContent", + false, + enumToInspectableSet()) + private val PositionTypeId = + MetadataRegister.register( + MetadataRegister.TYPE_ATTRIBUTE, + NAMESPACE, + "positionType", + false, + enumToInspectableSet()) + + private val FlexGrowId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexGrow") + private val FlexShrinkId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexShrink") + private val FlexBasisId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "flexBasis") + private val WidthId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "width") + private val HeightId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "height") + private val MinWidthId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minWidth") + private val MinHeightId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minHeight") + private val MaxWidthId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxWidth") + private val MaxHeightId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxHeight") + private val AspectRatioId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "aspectRatio") + + private val MarginId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "margin") + private val PaddingId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "padding") + private val BorderId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "border") + private val PositionId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "position") + + private val LeftId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "left") + private val TopId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "top") + private val RightId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "right") + private val BottomId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "bottom") + private val StartId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "start") + private val EndId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "end") + private val HorizontalId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "horizontal") + private val VerticalId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "vertical") + private val AllId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "all") + + private val HasViewOutputId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "hasViewOutput") + private val AlphaId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "alpha") + private val ScaleId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "scale") + private val RotationId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "rotation") + + private val EmptyId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "") + private val NoneId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "none") + private val SizeId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "size") + private val ViewOutputId = + MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "viewOutput") + + fun getInspectableBox( + left: YogaValue?, + top: YogaValue?, + right: YogaValue?, + bottom: YogaValue?, + horizontal: YogaValue?, + vertical: YogaValue?, + all: YogaValue?, + start: YogaValue?, + end: YogaValue? + ): InspectableObject { + val props = mutableMapOf() + + var actualLeft = 0 + var actualTop = 0 + var actualRight = 0 + var actualBottom = 0 + + all?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualLeft = intValue + actualTop = intValue + actualRight = intValue + actualBottom = intValue + } + + props[AllId] = InspectableValue.Text(yogaValue.toString()) + } + } + + horizontal?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualLeft = intValue + actualRight = intValue + } + + props[HorizontalId] = InspectableValue.Text(yogaValue.toString()) + } + } + + vertical?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualTop = intValue + actualBottom = intValue + } + + props[VerticalId] = InspectableValue.Text(yogaValue.toString()) + } + } + + left?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualLeft = intValue + } + + props[LeftId] = InspectableValue.Text(yogaValue.toString()) + } + } + + right?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualRight = intValue + } + + props[RightId] = InspectableValue.Text(yogaValue.toString()) + } + } + + top?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualTop = intValue + } + + props[TopId] = InspectableValue.Text(yogaValue.toString()) + } + } + + bottom?.let { yogaValue -> + if (yogaValue.unit != YogaUnit.UNDEFINED) { + if (yogaValue.unit == YogaUnit.POINT || yogaValue.unit == YogaUnit.PERCENT) { + val intValue = yogaValue.value.toInt() + actualBottom = intValue + } + + props[BottomId] = InspectableValue.Text(yogaValue.toString()) + } + } + + props[EmptyId] = + InspectableValue.SpaceBox(SpaceBox(actualTop, actualRight, actualBottom, actualLeft)) + + return InspectableObject(props) + } + + fun getInspectableBoxRaw( + left: Float?, + top: Float?, + right: Float?, + bottom: Float?, + horizontal: Float?, + vertical: Float?, + all: Float?, + start: Float?, + end: Float? + ): InspectableObject { + val props = mutableMapOf() + + var actualLeft = 0 + var actualTop = 0 + var actualRight = 0 + var actualBottom = 0 + + all?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualLeft = intValue + actualTop = intValue + actualRight = intValue + actualBottom = intValue + props[AllId] = InspectableValue.Number(value) + } + } + + horizontal?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualLeft = intValue + actualRight = intValue + props[HorizontalId] = InspectableValue.Number(value) + } + } + + vertical?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualTop = intValue + actualBottom = intValue + props[VerticalId] = InspectableValue.Number(value) + } + } + + left?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualLeft = intValue + props[LeftId] = InspectableValue.Number(value) + } + } + + right?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualRight = intValue + props[RightId] = InspectableValue.Number(value) + } + } + + top?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualTop = intValue + props[TopId] = InspectableValue.Number(value) + } + } + + bottom?.let { value -> + if (!value.isNaN()) { + val intValue = value.toInt() + actualBottom = intValue + props[BottomId] = InspectableValue.Number(value) + } + } + + props[EmptyId] = + InspectableValue.SpaceBox(SpaceBox(actualTop, actualRight, actualBottom, actualLeft)) + + return InspectableObject(props) + } + + fun getProps(component: DebugComponent): Map { + val props = mutableMapOf() + + val layout = component.layoutNode ?: return props + + props[AlignItemsId] = InspectableValue.Enum(layout.alignItems.name) + props[AlignSelfId] = InspectableValue.Enum(layout.alignSelf.name) + props[AlignContentId] = InspectableValue.Enum(layout.alignContent.name) + + props[AspectRatioId] = InspectableValue.Text(layout.aspectRatio.toString()) + + layout.background?.let { drawable -> props[BackgroundId] = fromDrawable(drawable) } + + props[DirectionId] = InspectableValue.Enum(layout.layoutDirection.name) + + props[FlexBasisId] = InspectableValue.Text(layout.flexBasis.toString()) + props[FlexDirectionId] = InspectableValue.Enum(layout.flexDirection.name) + props[FlexGrowId] = InspectableValue.Text(layout.flexGrow.toString()) + props[FlexShrinkId] = InspectableValue.Text(layout.flexShrink.toString()) + + layout.foreground?.let { drawable -> props[ForegroundId] = fromDrawable(drawable) } + + props[JustifyContentId] = InspectableValue.Enum(layout.justifyContent.name) + + props[PositionTypeId] = InspectableValue.Enum(layout.positionType.name) + + val size: MutableMap = mutableMapOf() + size[WidthId] = InspectableValue.Text(layout.width.toString()) + if (layout.minWidth.unit != YogaUnit.UNDEFINED) + size[MinWidthId] = InspectableValue.Text(layout.minWidth.toString()) + if (layout.maxWidth.unit != YogaUnit.UNDEFINED) + size[MaxWidthId] = InspectableValue.Text(layout.maxWidth.toString()) + size[HeightId] = InspectableValue.Text(layout.height.toString()) + if (layout.minHeight.unit != YogaUnit.UNDEFINED) + size[MinHeightId] = InspectableValue.Text(layout.minHeight.toString()) + if (layout.maxHeight.unit != YogaUnit.UNDEFINED) + size[MaxHeightId] = InspectableValue.Text(layout.maxHeight.toString()) + + props[SizeId] = InspectableObject(size) + + props[MarginId] = + getInspectableBox( + layout.getMargin(YogaEdge.LEFT), + layout.getMargin(YogaEdge.TOP), + layout.getMargin(YogaEdge.RIGHT), + layout.getMargin(YogaEdge.BOTTOM), + layout.getMargin(YogaEdge.HORIZONTAL), + layout.getMargin(YogaEdge.VERTICAL), + layout.getMargin(YogaEdge.ALL), + layout.getMargin(YogaEdge.START), + layout.getMargin(YogaEdge.END)) + + props[PaddingId] = + getInspectableBox( + layout.getPadding(YogaEdge.LEFT), + layout.getPadding(YogaEdge.TOP), + layout.getPadding(YogaEdge.RIGHT), + layout.getPadding(YogaEdge.BOTTOM), + layout.getPadding(YogaEdge.HORIZONTAL), + layout.getPadding(YogaEdge.VERTICAL), + layout.getPadding(YogaEdge.ALL), + layout.getPadding(YogaEdge.START), + layout.getPadding(YogaEdge.END)) + + props[BorderId] = + getInspectableBoxRaw( + layout.getBorderWidth(YogaEdge.LEFT), + layout.getBorderWidth(YogaEdge.TOP), + layout.getBorderWidth(YogaEdge.RIGHT), + layout.getBorderWidth(YogaEdge.BOTTOM), + layout.getBorderWidth(YogaEdge.HORIZONTAL), + layout.getBorderWidth(YogaEdge.VERTICAL), + layout.getBorderWidth(YogaEdge.ALL), + layout.getBorderWidth(YogaEdge.START), + layout.getBorderWidth(YogaEdge.END)) + + props[PositionId] = + getInspectableBox( + layout.getPosition(YogaEdge.LEFT), + layout.getPosition(YogaEdge.TOP), + layout.getPosition(YogaEdge.RIGHT), + layout.getPosition(YogaEdge.BOTTOM), + layout.getPosition(YogaEdge.HORIZONTAL), + layout.getPosition(YogaEdge.VERTICAL), + layout.getPosition(YogaEdge.ALL), + layout.getPosition(YogaEdge.START), + layout.getPosition(YogaEdge.END)) + + val viewOutput: MutableMap = mutableMapOf() + viewOutput[HasViewOutputId] = InspectableValue.Boolean(layout.hasViewOutput()) + if (layout.hasViewOutput()) { + viewOutput[AlphaId] = InspectableValue.Number(layout.alpha) + viewOutput[RotationId] = InspectableValue.Number(layout.rotation) + viewOutput[ScaleId] = InspectableValue.Number(layout.scale) + } + props[ViewOutputId] = InspectableObject(viewOutput) + + return props + } + + private fun fromDrawable(d: Drawable?): Inspectable = + when (d) { + is ColorDrawable -> InspectableValue.Color(Color.fromColor(d.color)) + else -> InspectableValue.Unknown(d.toString()) + } +}