Skip to content

Commit

Permalink
Workaround for a crash when converting Compose nodes
Browse files Browse the repository at this point in the history
Summary:
For an unknown reason we're getting reports about `NoSuchMethodError`s happening when using ui-inspector code. I'm not able to reproduce it locally so I'm adding another try-catch to prevent it from crashing.
I hope that once we convince Google to properly publish ui-inspector as a standalone library and we won't have to copy this code manually to our monorepo, those issues will get resolved.

This diff also updates the ui-inspector code in flipper codebase which is needed for gradle builds.

Reviewed By: pengj

Differential Revision: D53701915

fbshipit-source-id: aced2a1ca311c6d5db01e48903ef9662649811b4
  • Loading branch information
zielinskimz authored and facebook-github-bot committed Feb 13, 2024
1 parent 8afb9ff commit 0d83935
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 64 deletions.
2 changes: 2 additions & 0 deletions android/plugins/jetpack-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ android {
implementation project(':android')
implementation "androidx.compose.ui:ui:$COMPOSE_VERSION"
implementation "androidx.compose.ui:ui-tooling:$COMPOSE_VERSION"
implementation "androidx.compose.ui:ui-tooling-data:$COMPOSE_VERSION"
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
implementation "androidx.collection:collection-ktx:1.4.0"
implementation project(':inspection-lib')
implementation deps.soloader
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ object AbstractComposeViewDescriptor : ChainedDescriptor<AbstractComposeView>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val layoutInspector = LayoutInspectorTree()
layoutInspector.hideSystemNodes = true
val composeNodes = transform(child, layoutInspector.convert(child), layoutInspector)
val composeNodes =
try {
transform(child, layoutInspector.convert(child), layoutInspector)
} catch (t: Throwable) {
listOf(
WarningMessage(
"Unknown error occurred while trying to inspect compose node: ${t.message}",
getBounds(node)))
}
return if (composeNodes.isNullOrEmpty()) {
listOf(
WarningMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ package facebook.internal.androidx.compose.ui.inspection.inspector

import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.collection.LongList
import androidx.collection.mutableIntObjectMapOf
import androidx.collection.mutableLongListOf
import androidx.collection.mutableLongObjectMapOf
import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.CompositionGroup
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.R
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.inspection.util.AnchorMap
import androidx.compose.ui.inspection.util.NO_ANCHOR_ID
import androidx.compose.ui.layout.GraphicLayerInfo
import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.node.InteroperableComposeUiNode
Expand All @@ -42,8 +48,6 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
import facebook.internal.androidx.compose.ui.inspection.util.AnchorMap
import facebook.internal.androidx.compose.ui.inspection.util.NO_ANCHOR_ID
import java.util.ArrayDeque
import java.util.Collections
import java.util.IdentityHashMap
Expand Down Expand Up @@ -93,9 +97,9 @@ class LayoutInspectorTree {
/** Map from owner node to child trees that are about to be stitched to this owner */
private val ownerMap = IdentityHashMap<InspectorNode, MutableList<MutableInspectorNode>>()
/** Map from semantics id to a list of merged semantics information */
private val semanticsMap = mutableMapOf<Int, List<RawParameter>>()
private val semanticsMap = mutableIntObjectMapOf<List<RawParameter>>()
/* Map of seemantics id to a list of unmerged semantics information */
private val unmergedSemanticsMap = mutableMapOf<Int, List<RawParameter>>()
private val unmergedSemanticsMap = mutableIntObjectMapOf<List<RawParameter>>()
/** Set of tree nodes that were stitched into another tree */
private val stitched = Collections.newSetFromMap(IdentityHashMap<MutableInspectorNode, Boolean>())
private val contextCache = ContextCache()
Expand Down Expand Up @@ -397,7 +401,10 @@ class LayoutInspectorTree {
}
} else {
node.id = if (node.id != UNDEFINED_ID) node.id else --generatedId
val withSemantics = node.packageHash !in systemPackages
// START CHANGE
val withSemantics = true
// val withSemantics = node.packageHash !in systemPackages
// END CHANGE
val resultNode = node.build(withSemantics)
// TODO: replace getOrPut with putIfAbsent which requires API level 24
node.layoutNodes.forEach { claimedNodes.getOrPut(it) { resultNode } }
Expand Down Expand Up @@ -554,18 +561,30 @@ class LayoutInspectorTree {
return anchorId.toLong() - Int.MAX_VALUE.toLong() + RESERVED_FOR_GENERATED_IDS
}

private fun belongsToView(layoutNodes: List<LayoutInfo>, view: View): Boolean =
layoutNodes
.asSequence()
.flatMap { node ->
node
.getModifierInfo()
.asSequence()
.map { it.extra }
.filterIsInstance<GraphicLayerInfo>()
.map { it.ownerViewId }
}
.contains(view.uniqueDrawingId)
/**
* Returns true if the [layoutNodes] belong under the specified [view].
*
* For: popups & Dialogs we may encounter parts of a compose tree that belong under a different
* sub-composition. Consider these nodes to "belong" to the current sub-composition under [view]
* if the ownerViews contains [view] or doesn't contain any owner views at all.
*/
private fun belongsToView(layoutNodes: List<LayoutInfo>, view: View): Boolean {
val ownerViewIds = ownerViews(layoutNodes)
return ownerViewIds.isEmpty() || ownerViewIds.contains(view.uniqueDrawingId)
}

private fun ownerViews(layoutNodes: List<LayoutInfo>): LongList {
val ownerViewIds = mutableLongListOf()
layoutNodes.forEach { node ->
node.getModifierInfo().forEach { info ->
val extra = info.extra
if (extra is GraphicLayerInfo) {
ownerViewIds.add(extra.ownerViewId)
}
}
}
return ownerViewIds
}

private fun addParameters(context: SourceContext, node: MutableInspectorNode) {
context.parameters.forEach {
Expand Down Expand Up @@ -680,7 +699,7 @@ class LayoutInspectorTree {
* Map from View owner to a pair of [InspectorNode] indicating the actual root, and the node
* where the content should be stitched in.
*/
private val found = mutableMapOf<Long, InspectorNode>()
private val found = mutableLongObjectMapOf<InspectorNode>()

/** Call this before converting a SlotTree for an AndroidComposeView */
fun clear() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package facebook.internal.androidx.compose.ui.inspection.inspector

import facebook.internal.androidx.compose.ui.inspection.util.asIntArray
import androidx.collection.IntList

/**
* A reference to a parameter to a [NodeParameter]
Expand All @@ -33,16 +33,8 @@ class NodeParameterReference(
val anchorId: Int,
val kind: ParameterKind,
val parameterIndex: Int,
val indices: IntArray
val indices: IntList
) {
constructor(
nodeId: Long,
anchorId: Int,
kind: ParameterKind,
parameterIndex: Int,
indices: List<Int>
) : this(nodeId, anchorId, kind, parameterIndex, indices.asIntArray())

// For testing:
override fun toString(): String {
val suffix = if (indices.isNotEmpty()) ", ${indices.joinToString()}" else ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
package facebook.internal.androidx.compose.ui.inspection.inspector

import androidx.annotation.VisibleForTesting
import androidx.collection.intSetOf
import kotlin.math.absoluteValue

@VisibleForTesting
fun packageNameHash(packageName: String) =
packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue

val systemPackages =
setOf(
intSetOf(
-1,
packageNameHash("androidx.compose.animation"),
packageNameHash("androidx.compose.animation.core"),
Expand All @@ -37,7 +38,6 @@ val systemPackages =
packageNameHash("androidx.compose.foundation.text.selection"),
packageNameHash("androidx.compose.foundation.text2"),
packageNameHash("androidx.compose.foundation.text2.input"),
packageNameHash("androidx.compose.foundation.text2.input.internal.selection"),
packageNameHash("androidx.compose.foundation.window"),
packageNameHash("androidx.compose.material"),
packageNameHash("androidx.compose.material.internal"),
Expand All @@ -46,8 +46,9 @@ val systemPackages =
packageNameHash("androidx.compose.material3"),
packageNameHash("androidx.compose.material3.adaptive"),
packageNameHash("androidx.compose.material3.adaptive.navigation.suite"),
packageNameHash("androidx.compose.material3.common"),
packageNameHash("androidx.compose.material3.internal"),
packageNameHash("androidx.compose.material3.pullrefresh"),
packageNameHash("androidx.compose.material3.pulltorefresh"),
packageNameHash("androidx.compose.material3.windowsizeclass"),
packageNameHash("androidx.compose.runtime"),
packageNameHash("androidx.compose.runtime.livedata"),
Expand All @@ -66,4 +67,5 @@ val systemPackages =
packageNameHash("androidx.compose.ui.util"),
packageNameHash("androidx.compose.ui.viewinterop"),
packageNameHash("androidx.compose.ui.window"),
packageNameHash("androidx.navigation.compose"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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
Expand All @@ -28,6 +30,8 @@ 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
import androidx.compose.ui.inspection.util.copy
import androidx.compose.ui.inspection.util.removeLast
import androidx.compose.ui.platform.InspectableModifier
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.text.AnnotatedString
Expand Down Expand Up @@ -326,10 +330,10 @@ internal class ParameterFactory(private val inlineClassConverter: InlineClassCon
private var maxRecursions = 0
private var maxInitialIterableSize = 0
private var recursions = 0
private val valueIndex = mutableListOf<Int>()
private val valueIndex = mutableIntListOf()
private val valueLazyReferenceMap = IdentityHashMap<Any, MutableList<NodeParameter>>()
private val rootValueIndexCache =
mutableMapOf<Long, IdentityHashMap<Any, NodeParameterReference>>()
mutableLongObjectMapOf<IdentityHashMap<Any, NodeParameterReference>>()
private var valueIndexMap = IdentityHashMap<Any, NodeParameterReference>()

fun create(
Expand Down Expand Up @@ -373,12 +377,12 @@ internal class ParameterFactory(private val inlineClassConverter: InlineClassCon
maxInitialIterableSize)
var parent: Pair<String, Any?>? = null
var new = Pair(name, value)
for (i in reference.indices) {
reference.indices.forEach { index ->
parent = new
new = find(new.first, new.second, i) ?: return null
new = find(new.first, new.second, index) ?: return null
}
recursions = 0
valueIndex.addAll(reference.indices.asSequence())
valueIndex.addAll(reference.indices)
val parameter =
if (startIndex == 0) {
create(new.first, new.second, parent?.second)
Expand Down Expand Up @@ -609,7 +613,7 @@ internal class ParameterFactory(private val inlineClassConverter: InlineClassCon
}

private fun valueIndexToReference(): NodeParameterReference =
NodeParameterReference(nodeId, anchorId, kind, parameterIndex, valueIndex)
NodeParameterReference(nodeId, anchorId, kind, parameterIndex, valueIndex.copy())

private fun createEmptyParameter(name: String): NodeParameter =
NodeParameter(name, ParameterType.String, "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
* limitations under the License.
*/

package facebook.internal.androidx.compose.ui.inspection.util

import java.util.IdentityHashMap
package androidx.compose.ui.inspection.util

const val NO_ANCHOR_ID = 0

/** A map of anchors with a unique id generator. */
class AnchorMap {
private val anchorLookup = mutableMapOf<Int, Any>()
private val idLookup = IdentityHashMap<Any, Int>()
private val idLookup = mutableMapOf<Any, Int>()

/** Return a unique id for the specified [anchor] instance. */
operator fun get(anchor: Any?): Int =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2021 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 androidx.compose.ui.inspection.util

import androidx.collection.IntList
import androidx.collection.MutableIntList
import androidx.collection.MutableLongObjectMap
import androidx.collection.mutableIntListOf

fun MutableIntList.removeLast() {
val last = lastIndex
if (last < 0) throw NoSuchElementException("List is empty.") else removeAt(last)
}

fun <T, M : MutableLongObjectMap<MutableList<T>>> Iterable<T>.groupByToLongObjectMap(
destination: M,
keySelector: (T) -> Long
): M {
for (element in this) {
val key = keySelector(element)
val list = destination.getOrPut(key) { ArrayList() }
list.add(element)
}
return destination
}

fun IntList.copy(): IntList {
val result = mutableIntListOf()
forEach { result.add(it) }
return result
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package facebook.internal.androidx.compose.ui.inspection.util
package androidx.compose.ui.inspection.util

import android.os.Handler
import android.os.Looper
Expand Down

0 comments on commit 0d83935

Please sign in to comment.