From f64556e9d03adb6e8cdc0ddb2bd22574ff7f8d5d Mon Sep 17 00:00:00 2001 From: Michal Zielinski Date: Mon, 13 May 2024 06:08:29 -0700 Subject: [PATCH] Perform reflective parameter inspection for additional inspection nodes Summary: This diff uses the new Flipper API that allows for performing additional computation related to attributes on demand. By default the reflective parameter inspection is turned off which makes Flipper Compose plugin to perform well. User will have an ability to select a specific node in Flipper Desktop UI for which additional computation should be performed. When this happens Flipper Compose plugin will use reflection in order to perform additional analysis only on the selected node. The reflection is still slow but this way the user is in control and if they need to see additional information about some node, they can opt in. This diff brings back `ReflectionScope` class and adds back methods that perform reflective analysis to `ParameterFactory`. Reviewed By: pengj Differential Revision: D57148571 fbshipit-source-id: e53d734b6f9d6c64f69064104765a8089c4f0203 --- .../UIDebuggerComposeSupport.kt | 19 +- .../AbstractComposeViewDescriptor.kt | 19 +- .../descriptors/ComposeNodeDescriptor.kt | 15 +- .../jetpackcompose/model/ComposeNode.kt | 60 +++- .../inspector/LayoutInspectorTree.kt | 8 +- .../ui/inspection/inspector/NodeParameter.kt | 1 + ...arameterFactory.kt => ParameterFactory.kt} | 337 ++++++++++++++++-- .../inspection/inspector/ReflectionScope.kt | 215 +++++++++++ .../uidebugger/core/LayoutTraversal.kt | 14 +- 9 files changed, 626 insertions(+), 62 deletions(-) rename android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/{ReflectionFreeParameterFactory.kt => ParameterFactory.kt} (65%) create mode 100644 android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt index b77d08bb273..27ce60fcddd 100644 --- a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt +++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/UIDebuggerComposeSupport.kt @@ -44,19 +44,22 @@ object UIDebuggerComposeSupport { } private fun addDescriptors(register: DescriptorRegister) { - register.register(AbstractComposeView::class.java, AbstractComposeViewDescriptor) - register.register(ComposeNode::class.java, ComposeNodeDescriptor) - register.register(ComposeInnerViewNode::class.java, ComposeInnerViewDescriptor) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + register.register(AbstractComposeView::class.java, AbstractComposeViewDescriptor) + register.register(ComposeNode::class.java, ComposeNodeDescriptor) + register.register(ComposeInnerViewNode::class.java, ComposeInnerViewDescriptor) + } } private fun addCustomActions(context: UIDContext) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { context.addCustomActionGroup("Compose options", ActionIcon.Local("icons/compose-logo.png")) { - booleanAction("Hide System Nodes", AbstractComposeViewDescriptor.hideSystemNodes) { newValue - -> - AbstractComposeViewDescriptor.hideSystemNodes = newValue - newValue - } + booleanAction( + "Hide System Nodes", AbstractComposeViewDescriptor.layoutInspector.hideSystemNodes) { + newValue -> + AbstractComposeViewDescriptor.layoutInspector.hideSystemNodes = newValue + newValue + } } } } diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/AbstractComposeViewDescriptor.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/AbstractComposeViewDescriptor.kt index edce98ef544..ea442aa8f88 100644 --- a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/AbstractComposeViewDescriptor.kt +++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/AbstractComposeViewDescriptor.kt @@ -22,19 +22,17 @@ import facebook.internal.androidx.compose.ui.inspection.inspector.InspectorNode import facebook.internal.androidx.compose.ui.inspection.inspector.LayoutInspectorTree import java.io.IOException -@RequiresApi(Build.VERSION_CODES.Q) object AbstractComposeViewDescriptor : ChainedDescriptor() { - internal var hideSystemNodes: Boolean = true + @RequiresApi(Build.VERSION_CODES.Q) internal val layoutInspector = LayoutInspectorTree() - private val recompositionHandler by lazy { - RecompositionHandler(DefaultArtTooling("Flipper")).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - attachJvmtiAgent() - startTrackingRecompositions(this) + private val recompositionHandler = + RecompositionHandler(DefaultArtTooling("Flipper")).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + attachJvmtiAgent() + startTrackingRecompositions(this) + } } - } - } override fun onGetName(node: AbstractComposeView): String = node.javaClass.simpleName @@ -69,8 +67,7 @@ object AbstractComposeViewDescriptor : ChainedDescriptor() children.add(child) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val layoutInspector = LayoutInspectorTree() - layoutInspector.hideSystemNodes = hideSystemNodes + layoutInspector.resetAccumulativeState() val composeNodes = try { transform(child, layoutInspector.convert(child), layoutInspector) diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt index 25a7d807886..109209d33a9 100644 --- a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt +++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/descriptors/ComposeNodeDescriptor.kt @@ -12,6 +12,7 @@ import androidx.annotation.RequiresApi import com.facebook.flipper.plugins.jetpackcompose.JetpackComposeTag import com.facebook.flipper.plugins.jetpackcompose.descriptors.ComposeNodeDescriptor.toInspectableValue import com.facebook.flipper.plugins.jetpackcompose.model.ComposeNode +import com.facebook.flipper.plugins.uidebugger.descriptors.AttributesInfo import com.facebook.flipper.plugins.uidebugger.descriptors.BaseTags import com.facebook.flipper.plugins.uidebugger.descriptors.Id import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister @@ -78,7 +79,10 @@ object ComposeNodeDescriptor : NodeDescriptor { return node.children } - override fun getAttributes(node: ComposeNode): MaybeDeferred> { + override fun getAttributes( + node: ComposeNode, + shouldGetAdditionalData: Boolean + ): MaybeDeferred { return Deferred { val builder = mutableMapOf() val props = mutableMapOf() @@ -115,26 +119,26 @@ object ComposeNodeDescriptor : NodeDescriptor { } val params = mutableMapOf() - node.parameters.forEach { parameter -> + node.getParameters(shouldGetAdditionalData).forEach { parameter -> fillNodeParameters(parameter, params, node.inspectorNode.name) } builder[ParametersAttributeId] = InspectableObject(params.toMap()) val mergedSemantics = mutableMapOf() - node.mergedSemantics.forEach { parameter -> + node.getMergedSemantics(shouldGetAdditionalData).forEach { parameter -> fillNodeParameters(parameter, mergedSemantics, node.inspectorNode.name) } builder[MergedSemanticsAttributeId] = InspectableObject(mergedSemantics.toMap()) val unmergedSemantics = mutableMapOf() - node.unmergedSemantics.forEach { parameter -> + node.getUnmergedSemantics(shouldGetAdditionalData).forEach { parameter -> fillNodeParameters(parameter, unmergedSemantics, node.inspectorNode.name) } builder[UnmergedSemanticsAttributeId] = InspectableObject(unmergedSemantics.toMap()) builder[SectionId] = InspectableObject(props.toMap()) - builder + AttributesInfo(builder, node.hasAdditionalData) } } @@ -189,6 +193,7 @@ object ComposeNodeDescriptor : NodeDescriptor { private fun NodeParameter.toInspectableValue(): InspectableValue { return when (type) { ParameterType.Iterable, + ParameterType.ComplexObject, ParameterType.String -> InspectableValue.Text(value.toString()) ParameterType.Boolean -> InspectableValue.Boolean(value as Boolean) ParameterType.Int32 -> InspectableValue.Number(value as Int) diff --git a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt index 8a3e7c0d40a..61fc44bb729 100644 --- a/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt +++ b/android/plugins/jetpack-compose/src/main/java/com/facebook/flipper/plugins/jetpackcompose/model/ComposeNode.kt @@ -18,6 +18,7 @@ import facebook.internal.androidx.compose.ui.inspection.inspector.InspectorNode import facebook.internal.androidx.compose.ui.inspection.inspector.LayoutInspectorTree import facebook.internal.androidx.compose.ui.inspection.inspector.NodeParameter import facebook.internal.androidx.compose.ui.inspection.inspector.ParameterKind +import facebook.internal.androidx.compose.ui.inspection.inspector.ParameterType // Same values as in AndroidX (ComposeLayoutInspector.kt) private const val MAX_RECURSIONS = 2 @@ -47,21 +48,51 @@ class ComposeNode( val children: List = collectChildren() - val parameters: List by lazy { getNodeParameters(ParameterKind.Normal) } + val hasAdditionalData: Boolean + get() { + return hasAdditionalParameterData || + hasAdditionalMergedSemanticsData || + hasAdditionalUnmergedSemanticsData + } + + private var hasAdditionalParameterData: Boolean = false + private var hasAdditionalMergedSemanticsData: Boolean = false + private var hasAdditionalUnmergedSemanticsData: Boolean = false - val mergedSemantics: List by lazy { - getNodeParameters(ParameterKind.MergedSemantics) + fun getParameters(useReflection: Boolean): List { + return getNodeParameters(ParameterKind.Normal, useReflection) } - val unmergedSemantics: List by lazy { - getNodeParameters(ParameterKind.UnmergedSemantics) + fun getMergedSemantics(useReflection: Boolean): List { + return getNodeParameters(ParameterKind.MergedSemantics, useReflection) } - private fun getNodeParameters(kind: ParameterKind): List { + fun getUnmergedSemantics(useReflection: Boolean): List { + return getNodeParameters(ParameterKind.UnmergedSemantics, useReflection) + } + + private fun getNodeParameters(kind: ParameterKind, useReflection: Boolean): List { layoutInspectorTree.resetAccumulativeState() return try { - layoutInspectorTree.convertParameters( - inspectorNode.id, inspectorNode, kind, MAX_RECURSIONS, MAX_ITERABLE_SIZE) + val params = + layoutInspectorTree.convertParameters( + inspectorNode.id, + inspectorNode, + kind, + MAX_RECURSIONS, + MAX_ITERABLE_SIZE, + useReflection) + if (!useReflection) { + // We only need to check for additional data if we are not using reflection since + // params parsed with useReflection == true wont have complex objects + val hasAdditionalData = hasAdditionalData(params) + when (kind) { + ParameterKind.Normal -> hasAdditionalParameterData = hasAdditionalData + ParameterKind.MergedSemantics -> hasAdditionalMergedSemanticsData = hasAdditionalData + ParameterKind.UnmergedSemantics -> hasAdditionalUnmergedSemanticsData = hasAdditionalData + } + } + params } catch (t: Throwable) { Log.e(TAG, "Failed to get parameters.", t) emptyList() @@ -107,6 +138,19 @@ class ComposeNode( return null } + private fun hasAdditionalData(params: List): Boolean { + val queue = ArrayDeque() + queue.addAll(params) + while (!queue.isEmpty()) { + val param = queue.removeFirst() + if (param.type == ParameterType.ComplexObject) { + return true + } + queue.addAll(param.elements) + } + return false + } + companion object { private const val TAG = "ComposeNode" } diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt index 48eca52c231..8d4736f38b4 100644 --- a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt +++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt @@ -89,7 +89,7 @@ class LayoutInspectorTree { private var foundNode: InspectorNode? = null private var windowSize = emptySize private val inlineClassConverter = InlineClassConverter() - private val parameterFactory = ReflectionFreeParameterFactory() + private val parameterFactory = ParameterFactory(inlineClassConverter) private val cache = ArrayDeque() private var generatedId = -1L private val subCompositions = SubCompositionRoots() @@ -168,7 +168,8 @@ class LayoutInspectorTree { node: InspectorNode, kind: ParameterKind, maxRecursions: Int, - maxInitialIterableSize: Int + maxInitialIterableSize: Int, + useReflection: Boolean ): List { val parameters = node.parametersByKind(kind) return parameters.mapIndexed { index, parameter -> @@ -181,7 +182,8 @@ class LayoutInspectorTree { kind, index, maxRecursions, - maxInitialIterableSize) + maxInitialIterableSize, + useReflection) } } diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt index b2a3ce1a040..e4807083e7c 100644 --- a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt +++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/NodeParameter.kt @@ -54,4 +54,5 @@ enum class ParameterType { Lambda, FunctionReference, Iterable, + ComplexObject, } diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionFreeParameterFactory.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt similarity index 65% rename from android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionFreeParameterFactory.kt rename to android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt index a996d8b5fe9..356bab0d4be 100644 --- a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionFreeParameterFactory.kt +++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ParameterFactory.kt @@ -16,13 +16,17 @@ package facebook.internal.androidx.compose.ui.inspection.inspector +import android.util.Log import android.view.View import androidx.collection.mutableIntListOf import androidx.collection.mutableLongObjectMapOf import androidx.compose.runtime.internal.ComposableLambda +import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector @@ -36,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.ResourceFont import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit @@ -43,24 +48,71 @@ import androidx.compose.ui.unit.TextUnitType import facebook.internal.androidx.compose.ui.inspection.inspector.ParameterType.DimensionDp import facebook.internal.androidx.compose.ui.inspection.util.copy import facebook.internal.androidx.compose.ui.inspection.util.removeLast +import java.lang.reflect.Field +import java.lang.reflect.Modifier as JavaModifier import java.util.IdentityHashMap import kotlin.jvm.internal.FunctionReference import kotlin.jvm.internal.Lambda import kotlin.math.abs +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.allSuperclasses +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaGetter /** * Factory of [NodeParameter]s. * * Each parameter value is converted to a user readable value. - * - * Note: This file has been modified so that it doesn't rely on Kotlin reflection API. */ -internal class ReflectionFreeParameterFactory { +internal class ParameterFactory(private val inlineClassConverter: InlineClassConverter) { + + private val reflectionScope: ReflectionScope = ReflectionScope() + + /** A map from known values to a user readable string representation. */ + private val valueLookup = mutableMapOf() + /** The classes we have loaded constants from. */ + private val valuesLoaded = mutableSetOf>() + + /** + * Do not load constant names from instances of these classes. We prefer showing the raw values of + * Color and Dimensions. + */ + private val ignoredClasses = listOf(Color::class.java, Dp::class.java) private var creatorCache: ParameterCreator? = null + /** + * Do not decompose instances or lookup constants from these package prefixes + * + * The following instances are known to contain self recursion: + * - kotlinx.coroutines.flow.StateFlowImpl + * - androidx.compose.ui.node.LayoutNode + */ + private val ignoredPackagePrefixes = + listOf("android.", "java.", "javax.", "kotlinx.", "androidx.compose.ui.node.") + var density = Density(1.0f) + init { + val textDecorationCombination = + TextDecoration.combine(listOf(TextDecoration.LineThrough, TextDecoration.Underline)) + valueLookup[textDecorationCombination] = "LineThrough+Underline" + valueLookup[Color.Unspecified] = "Unspecified" + valueLookup[RectangleShape] = "RectangleShape" + valuesLoaded.add(Enum::class.java) + valuesLoaded.add(Any::class.java) + + // AbsoluteAlignment is not found from an instance of BiasAbsoluteAlignment, + // because Alignment has no file level class. + reflectionScope.withReflectiveAccess { + loadConstantsFromEnclosedClasses(AbsoluteAlignment::class.java) + } + } + /** * Create a [NodeParameter] from the specified parameter [name] and [value]. * @@ -76,30 +128,169 @@ internal class ReflectionFreeParameterFactory { kind: ParameterKind, parameterIndex: Int, maxRecursions: Int, - maxInitialIterableSize: Int + maxInitialIterableSize: Int, + useReflection: Boolean ): NodeParameter { val creator = creatorCache ?: ParameterCreator() try { - return creator.create( - rootId, - nodeId, - anchorId, - name, - value, - kind, - parameterIndex, - maxRecursions, - maxInitialIterableSize) + return if (useReflection) { + reflectionScope.withReflectiveAccess { + creator.create( + rootId, + nodeId, + anchorId, + name, + value, + kind, + parameterIndex, + maxRecursions, + maxInitialIterableSize, + true) + } + } else { + creator.create( + rootId, + nodeId, + anchorId, + name, + value, + kind, + parameterIndex, + maxRecursions, + maxInitialIterableSize, + false) + } } finally { creatorCache = creator } } fun clearReferenceCache() { - val creator = creatorCache ?: return - creator.clearReferenceCache() + creatorCache?.clearReferenceCache() + } + + private fun loadConstantsFrom(javaClass: Class<*>) { + if (valuesLoaded.contains(javaClass) || + ignoredPackagePrefixes.any { javaClass.name.startsWith(it) }) { + return + } + val related = generateSequence(javaClass) { it.superclass }.plus(javaClass.interfaces) + related.forEach { aClass -> + val topClass = generateSequence(aClass) { safeEnclosingClass(it) }.last() + loadConstantsFromEnclosedClasses(topClass) + findPackageLevelClass(topClass)?.let { loadConstantsFromStaticFinal(it) } + } + } + + private fun safeEnclosingClass(klass: Class<*>): Class<*>? = + try { + klass.enclosingClass + } catch (_: Error) { + // Exceptions seen on API 23... + null + } + + private fun findPackageLevelClass(javaClass: Class<*>): Class<*>? = + try { + // Note: This doesn't work when @file.JvmName is specified + Class.forName("${javaClass.name}Kt") + } catch (ex: Throwable) { + null + } + + private fun loadConstantsFromEnclosedClasses(javaClass: Class<*>) { + if (valuesLoaded.contains(javaClass)) { + return + } + loadConstantsFromObjectInstance(javaClass.kotlin) + loadConstantsFromStaticFinal(javaClass) + valuesLoaded.add(javaClass) + javaClass.declaredClasses.forEach { loadConstantsFromEnclosedClasses(it) } } + /** + * Load all constants from companion objects and singletons + * + * Exclude: primary types and types of ignoredClasses, open and lateinit vals. + */ + private fun loadConstantsFromObjectInstance(kClass: KClass<*>) { + try { + val instance = kClass.objectInstance ?: return + kClass.declaredMemberProperties + .asSequence() + .filter { it.isFinal && !it.isLateinit } + .mapNotNull { constantValueOf(it, instance)?.let { key -> Pair(key, it.name) } } + .filter { !ignoredValue(it.first) } + .toMap(valueLookup) + } catch (_: Throwable) { + // KT-16479 : kotlin reflection does currently not support packages and files. + // We load top level values using Java reflection instead. + // Ignore other reflection errors as well + } + } + + /** + * Load all constants from top level values from Java. + * + * Exclude: primary types and types of ignoredClasses. Since this is Java, inline types will also + * (unfortunately) be excluded. + */ + private fun loadConstantsFromStaticFinal(javaClass: Class<*>) { + try { + javaClass.declaredMethods + .asSequence() + .filter { + it.returnType != Void.TYPE && + JavaModifier.isStatic(it.modifiers) && + JavaModifier.isFinal(it.modifiers) && + !it.returnType.isPrimitive && + it.parameterTypes.isEmpty() && + it.name.startsWith("get") + } + .mapNotNull { javaClass.getDeclaredField(it.name.substring(3)) } + .mapNotNull { constantValueOf(it)?.let { key -> Pair(key, it.name) } } + .filter { !ignoredValue(it.first) } + .toMap(valueLookup) + } catch (_: ReflectiveOperationException) { + // ignore reflection errors + } catch (_: NoClassDefFoundError) { + // ignore missing classes on lower level SDKs + } + } + + private fun constantValueOf(field: Field?): Any? = + try { + field?.isAccessible = true + field?.get(null) + } catch (_: ReflectiveOperationException) { + // ignore reflection errors + null + } + + private fun constantValueOf(property: KProperty1, instance: Any): Any? = + try { + val field = property.javaField + field?.isAccessible = true + inlineClassConverter.castParameterValue(inlineResultClass(property), field?.get(instance)) + } catch (_: ReflectiveOperationException) { + // ignore reflection errors + null + } + + private fun inlineResultClass(property: KProperty1): String? { + // The Java getter name will be mangled if it contains parameters of an inline class. + // The mangled part starts with a '-'. + if (property.javaGetter?.name?.contains('-') == true) { + return property.returnType.toString() + } + return null + } + + private fun ignoredValue(value: Any?): Boolean = + value == null || + ignoredClasses.any { ignored -> ignored.isInstance(value) } || + value::class.java.isPrimitive + /** Convenience class for building [NodeParameter]s. */ private inner class ParameterCreator { private var rootId = 0L @@ -115,6 +306,7 @@ internal class ReflectionFreeParameterFactory { private val rootValueIndexCache = mutableLongObjectMapOf>() private var valueIndexMap = IdentityHashMap() + private var useReflection = false fun create( rootId: Long, @@ -125,11 +317,19 @@ internal class ReflectionFreeParameterFactory { kind: ParameterKind, parameterIndex: Int, maxRecursions: Int, - maxInitialIterableSize: Int + maxInitialIterableSize: Int, + useReflection: Boolean ): NodeParameter = try { setup( - rootId, nodeId, anchorId, kind, parameterIndex, maxRecursions, maxInitialIterableSize) + rootId, + nodeId, + anchorId, + kind, + parameterIndex, + maxRecursions, + maxInitialIterableSize, + useReflection) create(name, value, null) ?: createEmptyParameter(name) } finally { setup() @@ -146,7 +346,8 @@ internal class ReflectionFreeParameterFactory { newKind: ParameterKind = ParameterKind.Normal, newParameterIndex: Int = 0, maxRecursions: Int = 0, - maxInitialIterableSize: Int = 0 + maxInitialIterableSize: Int = 0, + useReflection: Boolean = false ) { rootId = newRootId nodeId = newNodeId @@ -159,6 +360,7 @@ internal class ReflectionFreeParameterFactory { valueIndex.clear() valueLazyReferenceMap.clear() valueIndexMap = rootValueIndexCache.getOrPut(newRootId) { IdentityHashMap() } + this.useReflection = useReflection } private fun create(name: String, value: Any?, parentValue: Any?): NodeParameter? { @@ -188,7 +390,11 @@ internal class ReflectionFreeParameterFactory { if (value == null) { return null } - + if (useReflection) { + createFromConstant(name, value)?.let { + return it + } + } return when (value) { is AnnotatedString -> NodeParameter(name, ParameterType.String, value.text) is BaselineShift -> createFromBaselineShift(name, value) @@ -234,8 +440,21 @@ internal class ReflectionFreeParameterFactory { createFromSequence(name, value, value.asSequence(), startIndex, maxElements) value.javaClass.isArray -> createFromArray(name, value, startIndex, maxElements) value is Offset -> createFromOffset(name, value) + value is Shadow -> { + if (useReflection) { + createFromShadow(name, value) + } else { + NodeParameter(name, ParameterType.ComplexObject, value.toString()) + } + } value is TextStyle -> createFromTextStyle(name, value) - else -> NodeParameter(name, ParameterType.String, value.toString()) + else -> { + if (useReflection) { + createFromKotlinReflection(name, value) + } else { + NodeParameter(name, ParameterType.ComplexObject, value.toString()) + } + } } private fun createRecursively( @@ -382,6 +601,11 @@ internal class ReflectionFreeParameterFactory { null } + private fun createFromConstant(name: String, value: Any): NodeParameter? { + loadConstantsFrom(value.javaClass) + return valueLookup[value]?.let { NodeParameter(name, ParameterType.String, it) } + } + // For now: select ResourceFontFont closest to W400 and Normal, and return the resId private fun createFromFontListFamily(name: String, value: FontListFontFamily): NodeParameter? = findBestResourceFont(value)?.let { NodeParameter(name, ParameterType.Resource, it.resId) } @@ -389,6 +613,63 @@ internal class ReflectionFreeParameterFactory { private fun createFromFunctionReference(name: String, value: FunctionReference): NodeParameter = NodeParameter(name, ParameterType.FunctionReference, arrayOf(value, value.name)) + private fun createFromKotlinReflection(name: String, value: Any): NodeParameter? { + val simpleName = value::class.simpleName + val properties = lookup(value) ?: return null + val parameter = NodeParameter(name, ParameterType.String, simpleName) + return when { + properties.isEmpty() -> parameter + !shouldRecurseDeeper() -> parameter.withChildReference(value) + else -> { + val elements = parameter.store(value).elements + properties.values.mapIndexedNotNullTo(elements) { index, part -> + createRecursively(part.name, valueOf(part, value), value, index) + } + parameter.removeIfEmpty(value) + } + } + } + + private fun lookup(value: Any): Map>? { + val kClass = value::class + val simpleName = kClass.simpleName + val qualifiedName = kClass.qualifiedName + if (simpleName == null || + qualifiedName == null || + ignoredPackagePrefixes.any { qualifiedName.startsWith(it) } || + kClass.allSuperclasses.any { superClass -> + val superClassQualifiedName = superClass.qualifiedName + superClassQualifiedName == null || + ignoredPackagePrefixes.any { superClassQualifiedName.startsWith(it) } + }) { + // Exit without creating a parameter for: + // - internal synthetic classes + // - certain android packages + return null + } + return try { + sequenceOf(kClass) + .plus(kClass.allSuperclasses.asSequence()) + .flatMap { it.declaredMemberProperties.asSequence() } + .associateBy { it.name } + } catch (ex: Throwable) { + Log.w("Compose", "Could not decompose ${kClass.simpleName}", ex) + null + } + } + + private fun valueOf(property: KProperty<*>, instance: Any): Any? = + try { + property.isAccessible = true + // Bug in kotlin reflection API: if the type is a nullable inline type with a null + // value, we get an IllegalArgumentException in this line: + property.getter.call(instance) + } catch (ex: Throwable) { + // TODO: Remove this warning since this is expected with nullable inline types + Log.w("Compose", "Could not get value of ${property.name}") + null + } + private fun createFromInspectableValue(name: String, value: InspectableValue): NodeParameter { val tempValue = value.valueOverride ?: "" val parameterName = name.ifEmpty { value.nameFallback } ?: "element" @@ -510,6 +791,20 @@ internal class ReflectionFreeParameterFactory { return parameter } + // Special handling of blurRadius: convert to dp: + private fun createFromShadow(name: String, value: Shadow): NodeParameter? { + val parameter = createFromKotlinReflection(name, value) ?: return null + val elements = parameter.elements + val index = elements.indexOfFirst { it.name == "blurRadius" } + if (index >= 0) { + val existing = elements[index] + val blurRadius = with(density) { value.blurRadius.toDp().value } + elements[index] = NodeParameter("blurRadius", DimensionDp, blurRadius) + elements[index].index = existing.index + } + return parameter + } + // Temporary handling of TextStyle: remove when TextStyle implements InspectableValue // Hide: paragraphStyle, spanStyle, platformStyle, lineHeightStyle private fun createFromTextStyle(name: String, value: TextStyle): NodeParameter? { diff --git a/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt new file mode 100644 index 00000000000..321d9958d94 --- /dev/null +++ b/android/plugins/jetpack-compose/src/main/java/facebook/internal/androidx/compose/ui/inspection/inspector/ReflectionScope.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package facebook.internal.androidx.compose.ui.inspection.inspector + +import android.annotation.SuppressLint +import java.lang.reflect.Field +import java.lang.reflect.Modifier +import kotlin.jvm.internal.FunctionBase +import kotlin.jvm.internal.FunctionReference +import kotlin.jvm.internal.Lambda +import kotlin.jvm.internal.MutablePropertyReference0 +import kotlin.jvm.internal.MutablePropertyReference1 +import kotlin.jvm.internal.MutablePropertyReference2 +import kotlin.jvm.internal.PropertyReference0 +import kotlin.jvm.internal.PropertyReference1 +import kotlin.jvm.internal.PropertyReference2 +import kotlin.jvm.internal.Reflection +import kotlin.jvm.internal.ReflectionFactory +import kotlin.reflect.KClass +import kotlin.reflect.KClassifier +import kotlin.reflect.KDeclarationContainer +import kotlin.reflect.KFunction +import kotlin.reflect.KMutableProperty0 +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KMutableProperty2 +import kotlin.reflect.KProperty0 +import kotlin.reflect.KProperty1 +import kotlin.reflect.KProperty2 +import kotlin.reflect.KType +import kotlin.reflect.KTypeParameter +import kotlin.reflect.KTypeProjection +import kotlin.reflect.KVariance +import kotlin.reflect.jvm.internal.ReflectionFactoryImpl + +/** + * Scope that allows to use jarjar-ed kotlin-reflect artifact that is shipped with inspector itself. + * + * Issue with kotlin-reflect. Many of reflective calls such as "foo::class" rely on static functions + * defined in kotlin-stdlib's Reflection.java that delegate to ReflectionFactory. In order to + * initialize that factory kotlin-stdlib statically detects presence or absence of kotlin-reflect in + * classloader and chooses a factory accordingly. If there is no kotlin-reflect, very limited + * version of ReflectionFactory is used. + * + * It is an issue for inspectors because they could be loaded after that factory is initialised, and + * even if they are loaded before, they live in a separate child classloader, thus kotlin-reflect in + * inspector wouldn't exist for kotlin-stdlib in app. + * + * First step to avoid the issue is using ReflectionFactoryImpl that is bundled with inspector. Code + * for that would be fairly simple, for example instead of directly calling + * `kClass.declaredMemberProperties`, correct instance of kClass should be obtained from factory: + * `factory.getOrCreateKotlinClass(kClass.java).declaredMemberProperties`. + * + * That would work if code that works with correct KClass full implementation would never try to + * access a default factory installed in Reflection.java. Unfortunately it is not true, it + * eventually calls `CallableReference.getOwner()` in stdlib that uses default factory. + * + * As a result we have to replace the factory in Reflection.java. To avoid issues with user's code + * factory that we setup is smart, by default it simply delegates to a factory that was previously + * installed. Only within `reflectionScope.withReflectiveAccess{ }` factory from kotlin-reflect is + * used. + */ +@SuppressLint("BanUncheckedReflection") +class ReflectionScope { + + companion object { + init { + allowHiddenApi() + } + } + + private val scopedReflectionFactory = installScopedReflectionFactory() + + /** Runs `block` with access to kotlin-reflect. */ + fun withReflectiveAccess(block: () -> T): T { + return scopedReflectionFactory.withMainFactory(block) + } + + private fun installScopedReflectionFactory(): ScopedReflectionFactory { + val factoryField = Reflection::class.java.getDeclaredField("factory") + factoryField.isAccessible = true + val original: ReflectionFactory = factoryField.get(null) as ReflectionFactory + val modifiersField: Field = Field::class.java.getDeclaredField("accessFlags") + modifiersField.isAccessible = true + // make field non-final 😅 b/179685774 https://youtrack.jetbrains.com/issue/KT-44795 + modifiersField.setInt(factoryField, factoryField.modifiers and Modifier.FINAL.inv()) + val scopedReflectionFactory = ScopedReflectionFactory(original) + factoryField.set(null, scopedReflectionFactory) + return scopedReflectionFactory + } +} + +@SuppressLint("BanUncheckedReflection") +private fun allowHiddenApi() { + try { + val vmDebug = Class.forName("dalvik.system.VMDebug") + val allowHiddenApiReflectionFrom = + vmDebug.getDeclaredMethod("allowHiddenApiReflectionFrom", Class::class.java) + allowHiddenApiReflectionFrom.invoke(null, ReflectionScope::class.java) + } catch (e: Throwable) { + // ignore failure, let's try to proceed without it + } +} + +private class ScopedReflectionFactory( + private val original: ReflectionFactory, +) : ReflectionFactory() { + private val mainFactory = ReflectionFactoryImpl() + private val threadLocalFactory = ThreadLocal() + + fun withMainFactory(block: () -> T): T { + threadLocalFactory.set(mainFactory) + try { + return block() + } finally { + threadLocalFactory.set(null) + } + } + + val factory: ReflectionFactory + get() = threadLocalFactory.get() ?: original + + override fun createKotlinClass(javaClass: Class<*>?): KClass<*> { + return factory.createKotlinClass(javaClass) + } + + override fun createKotlinClass(javaClass: Class<*>?, internalName: String?): KClass<*> { + return factory.createKotlinClass(javaClass, internalName) + } + + override fun getOrCreateKotlinPackage( + javaClass: Class<*>?, + moduleName: String? + ): KDeclarationContainer { + return factory.getOrCreateKotlinPackage(javaClass, moduleName) + } + + override fun getOrCreateKotlinClass(javaClass: Class<*>?): KClass<*> { + return factory.getOrCreateKotlinClass(javaClass) + } + + override fun getOrCreateKotlinClass(javaClass: Class<*>?, internalName: String?): KClass<*> { + return factory.getOrCreateKotlinClass(javaClass, internalName) + } + + override fun renderLambdaToString(lambda: Lambda<*>?): String { + return factory.renderLambdaToString(lambda) + } + + override fun renderLambdaToString(lambda: FunctionBase<*>?): String { + return factory.renderLambdaToString(lambda) + } + + override fun function(f: FunctionReference?): KFunction<*> { + return factory.function(f) + } + + override fun property0(p: PropertyReference0?): KProperty0<*> { + return factory.property0(p) + } + + override fun mutableProperty0(p: MutablePropertyReference0?): KMutableProperty0<*> { + return factory.mutableProperty0(p) + } + + override fun property1(p: PropertyReference1?): KProperty1<*, *> { + return factory.property1(p) + } + + override fun mutableProperty1(p: MutablePropertyReference1?): KMutableProperty1<*, *> { + return factory.mutableProperty1(p) + } + + override fun property2(p: PropertyReference2?): KProperty2<*, *, *> { + return factory.property2(p) + } + + override fun mutableProperty2(p: MutablePropertyReference2?): KMutableProperty2<*, *, *> { + return factory.mutableProperty2(p) + } + + override fun typeOf( + klass: KClassifier?, + arguments: MutableList?, + isMarkedNullable: Boolean + ): KType { + return factory.typeOf(klass, arguments, isMarkedNullable) + } + + override fun typeParameter( + container: Any?, + name: String?, + variance: KVariance?, + isReified: Boolean + ): KTypeParameter { + return factory.typeParameter(container, name, variance, isReified) + } + + override fun setUpperBounds(typeParameter: KTypeParameter?, bounds: MutableList?) { + factory.setUpperBounds(typeParameter, bounds) + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt index de270e5b486..3b94fc7f77e 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt @@ -101,12 +101,14 @@ class LayoutTraversal( visited.add( attributesInfo.map { attrsInfo -> val additionalDataStatus = - if (!attrsInfo.hasAdditionalData) { - AdditionalDataStatus.NOT_AVAILABLE - } else if (shouldGetAdditionalData) { - AdditionalDataStatus.ENABLED - } else { - AdditionalDataStatus.DISABLED + when (!shouldGetAdditionalData && !attrsInfo.hasAdditionalData) { + true -> AdditionalDataStatus.NOT_AVAILABLE + false -> { + when (shouldGetAdditionalData) { + true -> AdditionalDataStatus.ENABLED + false -> AdditionalDataStatus.DISABLED + } + } } Node( curId,