Re-sync with internal repository
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
This is a check-in of https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection
|
||||
|
||||
The classes are currently not exported but we rely on them for debug information.
|
||||
|
||||
Tree: 59746be8ea17d5753471bd285b3fbc9cf8ea7c31
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Converter for casting a parameter represented by its primitive value to its inline class type.
|
||||
*
|
||||
* For example: an androidx.compose.ui.graphics.Color instance is often represented by a long
|
||||
*/
|
||||
internal class InlineClassConverter {
|
||||
// Map from inline type name to inline class and conversion lambda
|
||||
private val typeMap = mutableMapOf<String, (Any) -> Any>()
|
||||
// Return value used in functions
|
||||
private val notInlineType: (Any) -> Any = { it }
|
||||
|
||||
/** Clear any cached data. */
|
||||
fun clear() {
|
||||
typeMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the specified [value] to a value of type [inlineClassName] if possible.
|
||||
*
|
||||
* @param inlineClassName the fully qualified name of the inline class.
|
||||
* @param value the value to convert to an instance of [inlineClassName].
|
||||
*/
|
||||
fun castParameterValue(inlineClassName: String?, value: Any?): Any? =
|
||||
if (value != null && inlineClassName != null) typeMapperFor(inlineClassName)(value) else value
|
||||
|
||||
private fun typeMapperFor(typeName: String): (Any) -> (Any) =
|
||||
typeMap.getOrPut(typeName) { loadTypeMapper(typeName.replace('.', '/')) }
|
||||
|
||||
private fun loadTypeMapper(className: String): (Any) -> Any {
|
||||
val javaClass = loadClassOrNull(className) ?: return notInlineType
|
||||
val create = javaClass.declaredConstructors.singleOrNull() ?: return notInlineType
|
||||
create.isAccessible = true
|
||||
return { value -> create.newInstance(value) }
|
||||
}
|
||||
|
||||
private fun loadClassOrNull(className: String): Class<*>? =
|
||||
try {
|
||||
javaClass.classLoader!!.loadClass(className)
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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 androidx.compose.ui.layout.LayoutInfo
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
|
||||
internal const val UNDEFINED_ID = 0L
|
||||
|
||||
internal val emptyBox = IntRect(0, 0, 0, 0)
|
||||
internal val outsideBox = IntRect(Int.MAX_VALUE, Int.MIN_VALUE, Int.MAX_VALUE, Int.MIN_VALUE)
|
||||
|
||||
/** Node representing a Composable for the Layout Inspector. */
|
||||
class InspectorNode
|
||||
internal constructor(
|
||||
/** The associated render node id or 0. */
|
||||
val id: Long,
|
||||
|
||||
/** The associated key for tracking recomposition counts. */
|
||||
val key: Int,
|
||||
|
||||
/**
|
||||
* The id of the associated anchor for tracking recomposition counts.
|
||||
*
|
||||
* An Anchor is a mechanism in the compose runtime that can identify a Group in the SlotTable
|
||||
* that is invariant to SlotTable updates. See [androidx.compose.runtime.Anchor] for more
|
||||
* information.
|
||||
*/
|
||||
val anchorId: Int,
|
||||
|
||||
/** The name of the Composable. */
|
||||
val name: String,
|
||||
|
||||
/** The fileName where the Composable was called. */
|
||||
val fileName: String,
|
||||
|
||||
/**
|
||||
* A hash of the package name to help disambiguate duplicate [fileName] values.
|
||||
*
|
||||
* This hash is calculated by,
|
||||
*
|
||||
* `packageName.fold(0) { hash, current -> hash * 31 + current.toInt() }?.absoluteValue`
|
||||
*
|
||||
* where the package name is the dotted name of the package. This can be used to disambiguate
|
||||
* which file is referenced by [fileName]. This number is -1 if there was no package hash
|
||||
* information generated such as when the file does not contain a package declaration.
|
||||
*/
|
||||
val packageHash: Int,
|
||||
|
||||
/** The line number where the Composable was called. */
|
||||
val lineNumber: Int,
|
||||
|
||||
/** The UTF-16 offset in the file where the Composable was called */
|
||||
val offset: Int,
|
||||
|
||||
/** The number of UTF-16 code point comprise the Composable call */
|
||||
val length: Int,
|
||||
|
||||
/** The bounding box of the Composable. */
|
||||
internal val box: IntRect,
|
||||
|
||||
/** The 4 corners of the polygon after transformations of the original rectangle. */
|
||||
val bounds: QuadBounds? = null,
|
||||
|
||||
/** True if the code for the Composable was inlined */
|
||||
val inlined: Boolean = false,
|
||||
|
||||
/** The parameters of this Composable. */
|
||||
val parameters: List<RawParameter>,
|
||||
|
||||
/** The id of a android View embedded under this node. */
|
||||
val viewId: Long,
|
||||
|
||||
/** The merged semantics information of this Composable. */
|
||||
val mergedSemantics: List<RawParameter>,
|
||||
|
||||
/** The un-merged semantics information of this Composable. */
|
||||
val unmergedSemantics: List<RawParameter>,
|
||||
|
||||
/** The children nodes of this Composable. */
|
||||
val children: List<InspectorNode>
|
||||
) {
|
||||
/** Left side of the Composable in pixels. */
|
||||
val left: Int
|
||||
get() = box.left
|
||||
|
||||
/** Top of the Composable in pixels. */
|
||||
val top: Int
|
||||
get() = box.top
|
||||
|
||||
/** Width of the Composable in pixels. */
|
||||
val width: Int
|
||||
get() = box.width
|
||||
|
||||
/** Width of the Composable in pixels. */
|
||||
val height: Int
|
||||
get() = box.height
|
||||
|
||||
fun parametersByKind(kind: ParameterKind): List<RawParameter> =
|
||||
when (kind) {
|
||||
ParameterKind.Normal -> parameters
|
||||
ParameterKind.MergedSemantics -> mergedSemantics
|
||||
ParameterKind.UnmergedSemantics -> unmergedSemantics
|
||||
}
|
||||
}
|
||||
|
||||
data class QuadBounds(
|
||||
val x0: Int,
|
||||
val y0: Int,
|
||||
val x1: Int,
|
||||
val y1: Int,
|
||||
val x2: Int,
|
||||
val y2: Int,
|
||||
val x3: Int,
|
||||
val y3: Int,
|
||||
) {
|
||||
val xMin: Int
|
||||
get() = sequenceOf(x0, x1, x2, x3).minOrNull()!!
|
||||
|
||||
val xMax: Int
|
||||
get() = sequenceOf(x0, x1, x2, x3).maxOrNull()!!
|
||||
|
||||
val yMin: Int
|
||||
get() = sequenceOf(y0, y1, y2, y3).minOrNull()!!
|
||||
|
||||
val yMax: Int
|
||||
get() = sequenceOf(y0, y1, y2, y3).maxOrNull()!!
|
||||
|
||||
val outerBox: IntRect
|
||||
get() = IntRect(xMin, yMin, xMax, yMax)
|
||||
}
|
||||
|
||||
/** Parameter definition with a raw value reference. */
|
||||
class RawParameter(val name: String, val value: Any?)
|
||||
|
||||
/** Mutable version of [InspectorNode]. */
|
||||
internal class MutableInspectorNode {
|
||||
var id = UNDEFINED_ID
|
||||
var key = 0
|
||||
var anchorId = 0
|
||||
val layoutNodes = mutableListOf<LayoutInfo>()
|
||||
val mergedSemantics = mutableListOf<RawParameter>()
|
||||
val unmergedSemantics = mutableListOf<RawParameter>()
|
||||
var name = ""
|
||||
var fileName = ""
|
||||
var packageHash = -1
|
||||
var lineNumber = 0
|
||||
var offset = 0
|
||||
var length = 0
|
||||
var box: IntRect = emptyBox
|
||||
var bounds: QuadBounds? = null
|
||||
var inlined = false
|
||||
val parameters = mutableListOf<RawParameter>()
|
||||
var viewId = UNDEFINED_ID
|
||||
val children = mutableListOf<InspectorNode>()
|
||||
var outerBox: IntRect = outsideBox
|
||||
|
||||
fun reset() {
|
||||
markUnwanted()
|
||||
id = UNDEFINED_ID
|
||||
key = 0
|
||||
anchorId = 0
|
||||
viewId = UNDEFINED_ID
|
||||
layoutNodes.clear()
|
||||
mergedSemantics.clear()
|
||||
unmergedSemantics.clear()
|
||||
box = emptyBox
|
||||
bounds = null
|
||||
inlined = false
|
||||
outerBox = outsideBox
|
||||
children.clear()
|
||||
}
|
||||
|
||||
fun markUnwanted() {
|
||||
name = ""
|
||||
fileName = ""
|
||||
packageHash = -1
|
||||
lineNumber = 0
|
||||
offset = 0
|
||||
length = 0
|
||||
parameters.clear()
|
||||
}
|
||||
|
||||
fun shallowCopy(node: InspectorNode): MutableInspectorNode = apply {
|
||||
id = node.id
|
||||
key = node.key
|
||||
anchorId = node.anchorId
|
||||
mergedSemantics.addAll(node.mergedSemantics)
|
||||
unmergedSemantics.addAll(node.unmergedSemantics)
|
||||
name = node.name
|
||||
fileName = node.fileName
|
||||
packageHash = node.packageHash
|
||||
lineNumber = node.lineNumber
|
||||
offset = node.offset
|
||||
length = node.length
|
||||
box = node.box
|
||||
bounds = node.bounds
|
||||
inlined = node.inlined
|
||||
parameters.addAll(node.parameters)
|
||||
viewId = node.viewId
|
||||
children.addAll(node.children)
|
||||
}
|
||||
|
||||
fun build(withSemantics: Boolean = true): InspectorNode =
|
||||
InspectorNode(
|
||||
id,
|
||||
key,
|
||||
anchorId,
|
||||
name,
|
||||
fileName,
|
||||
packageHash,
|
||||
lineNumber,
|
||||
offset,
|
||||
length,
|
||||
box,
|
||||
bounds,
|
||||
inlined,
|
||||
parameters.toList(),
|
||||
viewId,
|
||||
if (withSemantics) mergedSemantics.toList() else emptyList(),
|
||||
if (withSemantics) unmergedSemantics.toList() else emptyList(),
|
||||
children.toList())
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
/*
|
||||
* 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.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
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.layout.GraphicLayerInfo
|
||||
import androidx.compose.ui.layout.LayoutInfo
|
||||
import androidx.compose.ui.node.InteroperableComposeUiNode
|
||||
import androidx.compose.ui.node.Ref
|
||||
import androidx.compose.ui.node.RootForTest
|
||||
import androidx.compose.ui.platform.ViewRootForInspector
|
||||
import androidx.compose.ui.semantics.getAllSemanticsNodes
|
||||
import androidx.compose.ui.tooling.data.ContextCache
|
||||
import androidx.compose.ui.tooling.data.ParameterInformation
|
||||
import androidx.compose.ui.tooling.data.SourceContext
|
||||
import androidx.compose.ui.tooling.data.SourceLocation
|
||||
import androidx.compose.ui.tooling.data.UiToolingDataApi
|
||||
import androidx.compose.ui.tooling.data.findParameters
|
||||
import androidx.compose.ui.tooling.data.mapTree
|
||||
import androidx.compose.ui.unit.Density
|
||||
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
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* The [InspectorNode.id] will be populated with:
|
||||
* - the layerId from a LayoutNode if this exists
|
||||
* - an id generated from an Anchor instance from the SlotTree if this exists
|
||||
* - a generated id if none of the above ids are available
|
||||
*
|
||||
* The interval -10000..-2 is reserved for the generated ids.
|
||||
*/
|
||||
@VisibleForTesting const val RESERVED_FOR_GENERATED_IDS = -10000L
|
||||
const val PLACEHOLDER_ID = Long.MAX_VALUE
|
||||
|
||||
private val emptySize = IntSize(0, 0)
|
||||
|
||||
private val unwantedCalls =
|
||||
setOf(
|
||||
"CompositionLocalProvider",
|
||||
"Content",
|
||||
"Inspectable",
|
||||
"ProvideAndroidCompositionLocals",
|
||||
"ProvideCommonCompositionLocals",
|
||||
)
|
||||
|
||||
/** Generator of a tree for the Layout Inspector. */
|
||||
@OptIn(UiToolingDataApi::class)
|
||||
class LayoutInspectorTree {
|
||||
@Suppress("MemberVisibilityCanBePrivate") var hideSystemNodes = true
|
||||
var includeNodesOutsizeOfWindow = true
|
||||
var includeAllParameters = true
|
||||
private var foundNode: InspectorNode? = null
|
||||
private var windowSize = emptySize
|
||||
private val inlineClassConverter = InlineClassConverter()
|
||||
private val parameterFactory = ParameterFactory(inlineClassConverter)
|
||||
private val cache = ArrayDeque<MutableInspectorNode>()
|
||||
private var generatedId = -1L
|
||||
private val subCompositions = SubCompositionRoots()
|
||||
/** Map from [LayoutInfo] to the nearest [InspectorNode] that contains it */
|
||||
private val claimedNodes = IdentityHashMap<LayoutInfo, InspectorNode>()
|
||||
/** Map from parent tree to child trees that are about to be stitched together */
|
||||
private val treeMap = IdentityHashMap<MutableInspectorNode, MutableList<MutableInspectorNode>>()
|
||||
/** 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>>()
|
||||
/* Map of seemantics id to a list of unmerged semantics information */
|
||||
private val unmergedSemanticsMap = mutableMapOf<Int, List<RawParameter>>()
|
||||
/** Set of tree nodes that were stitched into another tree */
|
||||
private val stitched = Collections.newSetFromMap(IdentityHashMap<MutableInspectorNode, Boolean>())
|
||||
private val contextCache = ContextCache()
|
||||
private val anchorMap = AnchorMap()
|
||||
|
||||
/** Converts the [CompositionData] set held by [view] into a list of root nodes. */
|
||||
fun convert(view: View): List<InspectorNode> {
|
||||
windowSize = IntSize(view.width, view.height)
|
||||
parameterFactory.density = Density(view.context)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val tables =
|
||||
view.getTag(R.id.inspection_slot_table_set) as? Set<CompositionData> ?: return emptyList()
|
||||
clear()
|
||||
collectSemantics(view)
|
||||
val result = convert(tables, view)
|
||||
clear()
|
||||
return result
|
||||
}
|
||||
|
||||
fun findParameters(view: View, anchorId: Int): InspectorNode? {
|
||||
windowSize = IntSize(view.width, view.height)
|
||||
parameterFactory.density = Density(view.context)
|
||||
val identity = anchorMap[anchorId] ?: return null
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val tables = view.getTag(R.id.inspection_slot_table_set) as? Set<CompositionData> ?: return null
|
||||
val node = newNode().apply { this.anchorId = anchorId }
|
||||
val group = tables.firstNotNullOfOrNull { it.find(identity) } ?: return null
|
||||
group.findParameters(contextCache).forEach {
|
||||
val castedValue = castValue(it)
|
||||
node.parameters.add(RawParameter(it.name, castedValue))
|
||||
}
|
||||
return buildAndRelease(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the roots to sub compositions that may have been collected from a different SlotTree.
|
||||
*
|
||||
* See [SubCompositionRoots] for details.
|
||||
*/
|
||||
fun addSubCompositionRoots(view: View, nodes: List<InspectorNode>): List<InspectorNode> =
|
||||
subCompositions.addRoot(view, nodes)
|
||||
|
||||
/**
|
||||
* Extract the merged semantics for this semantics owner such that they can be added to compose
|
||||
* nodes during the conversion of Group nodes.
|
||||
*/
|
||||
private fun collectSemantics(view: View) {
|
||||
val root = view as? RootForTest ?: return
|
||||
val nodes = root.semanticsOwner.getAllSemanticsNodes(mergingEnabled = true)
|
||||
val unmergedNodes = root.semanticsOwner.getAllSemanticsNodes(mergingEnabled = false)
|
||||
nodes.forEach { node ->
|
||||
semanticsMap[node.id] = node.config.map { RawParameter(it.key.name, it.value) }
|
||||
}
|
||||
unmergedNodes.forEach { node ->
|
||||
unmergedSemanticsMap[node.id] = node.config.map { RawParameter(it.key.name, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts the [RawParameter]s of the [node] into displayable parameters. */
|
||||
fun convertParameters(
|
||||
rootId: Long,
|
||||
node: InspectorNode,
|
||||
kind: ParameterKind,
|
||||
maxRecursions: Int,
|
||||
maxInitialIterableSize: Int
|
||||
): List<NodeParameter> {
|
||||
val parameters = node.parametersByKind(kind)
|
||||
return parameters.mapIndexed { index, parameter ->
|
||||
parameterFactory.create(
|
||||
rootId,
|
||||
node.id,
|
||||
node.anchorId,
|
||||
parameter.name,
|
||||
parameter.value,
|
||||
kind,
|
||||
index,
|
||||
maxRecursions,
|
||||
maxInitialIterableSize)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a part of the [RawParameter] identified by [reference] into a displayable parameter.
|
||||
* If the parameter is some sort of a collection then [startIndex] and [maxElements] describes the
|
||||
* scope of the data returned.
|
||||
*/
|
||||
fun expandParameter(
|
||||
rootId: Long,
|
||||
node: InspectorNode,
|
||||
reference: NodeParameterReference,
|
||||
startIndex: Int,
|
||||
maxElements: Int,
|
||||
maxRecursions: Int,
|
||||
maxInitialIterableSize: Int
|
||||
): NodeParameter? {
|
||||
val parameters = node.parametersByKind(reference.kind)
|
||||
if (reference.parameterIndex !in parameters.indices) {
|
||||
return null
|
||||
}
|
||||
val parameter = parameters[reference.parameterIndex]
|
||||
return parameterFactory.expand(
|
||||
rootId,
|
||||
node.id,
|
||||
node.anchorId,
|
||||
parameter.name,
|
||||
parameter.value,
|
||||
reference,
|
||||
startIndex,
|
||||
maxElements,
|
||||
maxRecursions,
|
||||
maxInitialIterableSize)
|
||||
}
|
||||
|
||||
/** Reset any state accumulated between windows. */
|
||||
@Suppress("unused")
|
||||
fun resetAccumulativeState() {
|
||||
subCompositions.resetAccumulativeState()
|
||||
parameterFactory.clearReferenceCache()
|
||||
// Reset the generated id. Nodes are assigned an id if there isn't a layout node id present.
|
||||
generatedId = -1L
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
cache.clear()
|
||||
inlineClassConverter.clear()
|
||||
claimedNodes.clear()
|
||||
treeMap.clear()
|
||||
ownerMap.clear()
|
||||
semanticsMap.clear()
|
||||
unmergedSemanticsMap.clear()
|
||||
stitched.clear()
|
||||
subCompositions.clear()
|
||||
foundNode = null
|
||||
}
|
||||
|
||||
private fun convert(tables: Set<CompositionData>, view: View): List<InspectorNode> {
|
||||
val trees = tables.mapNotNull { convert(view, it) }
|
||||
return when (trees.size) {
|
||||
0 -> listOf()
|
||||
1 -> addTree(mutableListOf(), trees.single())
|
||||
else -> stitchTreesByLayoutInfo(trees)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stitch separate trees together using the [LayoutInfo]s found in the [CompositionData]s.
|
||||
*
|
||||
* Some constructs in Compose (e.g. ModalDrawer) will result is multiple [CompositionData]s. This
|
||||
* code will attempt to stitch the resulting [InspectorNode] trees together by looking at the
|
||||
* parent of each [LayoutInfo].
|
||||
*
|
||||
* If this algorithm is successful the result of this function will be a list with a single tree.
|
||||
*/
|
||||
private fun stitchTreesByLayoutInfo(trees: List<MutableInspectorNode>): List<InspectorNode> {
|
||||
val layoutToTreeMap = IdentityHashMap<LayoutInfo, MutableInspectorNode>()
|
||||
trees.forEach { tree -> tree.layoutNodes.forEach { layoutToTreeMap[it] = tree } }
|
||||
trees.forEach { tree ->
|
||||
val layout = tree.layoutNodes.lastOrNull()
|
||||
val parentLayout =
|
||||
generateSequence(layout) { it.parentInfo }
|
||||
.firstOrNull {
|
||||
val otherTree = layoutToTreeMap[it]
|
||||
otherTree != null && otherTree != tree
|
||||
}
|
||||
if (parentLayout != null) {
|
||||
val ownerNode = claimedNodes[parentLayout]
|
||||
val ownerTree = layoutToTreeMap[parentLayout]
|
||||
if (ownerNode != null && ownerTree != null) {
|
||||
ownerMap.getOrPut(ownerNode) { mutableListOf() }.add(tree)
|
||||
treeMap.getOrPut(ownerTree) { mutableListOf() }.add(tree)
|
||||
}
|
||||
}
|
||||
}
|
||||
var parentTree = findDeepParentTree()
|
||||
while (parentTree != null) {
|
||||
addSubTrees(parentTree)
|
||||
treeMap.remove(parentTree)
|
||||
parentTree = findDeepParentTree()
|
||||
}
|
||||
val result = mutableListOf<InspectorNode>()
|
||||
trees.asSequence().filter { !stitched.contains(it) }.forEach { addTree(result, it) }
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a parent tree where the children trees (to be stitched under the parent) are not a
|
||||
* parent themselves. Do this to avoid rebuilding the same tree more than once.
|
||||
*/
|
||||
private fun findDeepParentTree(): MutableInspectorNode? =
|
||||
treeMap.entries
|
||||
.asSequence()
|
||||
.filter { (_, children) -> children.none { treeMap.containsKey(it) } }
|
||||
.firstOrNull()
|
||||
?.key
|
||||
|
||||
private fun addSubTrees(tree: MutableInspectorNode) {
|
||||
for ((index, child) in tree.children.withIndex()) {
|
||||
tree.children[index] = addSubTrees(child) ?: child
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild [node] with any possible sub trees added (stitched in). Return the rebuild node, or
|
||||
* null if no changes were found in this node or its children. Lazily allocate the new node to
|
||||
* avoid unnecessary allocations.
|
||||
*/
|
||||
private fun addSubTrees(node: InspectorNode): InspectorNode? {
|
||||
var newNode: MutableInspectorNode? = null
|
||||
for ((index, child) in node.children.withIndex()) {
|
||||
val newChild = addSubTrees(child)
|
||||
if (newChild != null) {
|
||||
val newCopy = newNode ?: newNode(node)
|
||||
newCopy.children[index] = newChild
|
||||
newNode = newCopy
|
||||
}
|
||||
}
|
||||
val trees = ownerMap[node]
|
||||
if (trees == null && newNode == null) {
|
||||
return null
|
||||
}
|
||||
val newCopy = newNode ?: newNode(node)
|
||||
if (trees != null) {
|
||||
trees.forEach { addTree(newCopy.children, it) }
|
||||
stitched.addAll(trees)
|
||||
}
|
||||
return buildAndRelease(newCopy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [tree] to the end of the [out] list. The root nodes of [tree] may be a fake node that hold
|
||||
* a list of [LayoutInfo].
|
||||
*/
|
||||
private fun addTree(
|
||||
out: MutableList<InspectorNode>,
|
||||
tree: MutableInspectorNode
|
||||
): List<InspectorNode> {
|
||||
tree.children.forEach {
|
||||
if (it.name.isNotEmpty()) {
|
||||
out.add(it)
|
||||
} else {
|
||||
out.addAll(it.children)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun convert(view: View, table: CompositionData): MutableInspectorNode? {
|
||||
val fakeParent = newNode()
|
||||
val group = table.mapTree(::convert, contextCache) ?: return null
|
||||
addToParent(fakeParent, listOf(group), buildFakeChildNodes = true)
|
||||
return if (belongsToView(fakeParent.layoutNodes, view)) fakeParent else null
|
||||
}
|
||||
|
||||
private fun convert(
|
||||
group: CompositionGroup,
|
||||
context: SourceContext,
|
||||
children: List<MutableInspectorNode>
|
||||
): MutableInspectorNode {
|
||||
val parent = parse(group, context, children)
|
||||
subCompositions.captureNode(parent, context)
|
||||
addToParent(parent, children)
|
||||
return parent
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the nodes in [input] to the children of [parentNode]. Nodes without a reference to a
|
||||
* wanted Composable are skipped unless [buildFakeChildNodes]. A single skipped render id and
|
||||
* layoutNode will be added to [parentNode].
|
||||
*/
|
||||
private fun addToParent(
|
||||
parentNode: MutableInspectorNode,
|
||||
input: List<MutableInspectorNode>,
|
||||
buildFakeChildNodes: Boolean = false
|
||||
) {
|
||||
// If we're adding an unwanted node from the `input` to the parent node and it has a
|
||||
// View ID, then assign it to the parent view so that we don't lose the context that we
|
||||
// found a View as a descendant of the parent node. Most likely, there were one or more
|
||||
// unwanted intermediate nodes between the node that actually owns the Android View
|
||||
// and the desired node that the View should be associated with in the inspector. If
|
||||
// there's more than one input node with a View ID, we skip this step since it's
|
||||
// unclear how these views would be related.
|
||||
input
|
||||
.singleOrNull { it.viewId != UNDEFINED_ID }
|
||||
?.takeIf { node ->
|
||||
// Take if the node has been marked as unwanted
|
||||
node.id == UNDEFINED_ID
|
||||
}
|
||||
?.let { nodeWithView -> parentNode.viewId = nodeWithView.viewId }
|
||||
|
||||
var id: Long? = null
|
||||
input.forEach { node ->
|
||||
if (node.name.isEmpty() && !(buildFakeChildNodes && node.layoutNodes.isNotEmpty())) {
|
||||
parentNode.children.addAll(node.children)
|
||||
if (node.id > UNDEFINED_ID) {
|
||||
// If multiple siblings with a render ids are dropped:
|
||||
// Ignore them all. And delegate the drawing to a parent in the inspector.
|
||||
id = if (id == null) node.id else UNDEFINED_ID
|
||||
}
|
||||
} else {
|
||||
node.id = if (node.id != UNDEFINED_ID) node.id else --generatedId
|
||||
val withSemantics = node.packageHash !in systemPackages
|
||||
val resultNode = node.build(withSemantics)
|
||||
// TODO: replace getOrPut with putIfAbsent which requires API level 24
|
||||
node.layoutNodes.forEach { claimedNodes.getOrPut(it) { resultNode } }
|
||||
parentNode.children.add(resultNode)
|
||||
if (withSemantics) {
|
||||
node.mergedSemantics.clear()
|
||||
node.unmergedSemantics.clear()
|
||||
}
|
||||
}
|
||||
if (node.bounds != null && parentNode.box == node.box) {
|
||||
parentNode.bounds = node.bounds
|
||||
}
|
||||
parentNode.layoutNodes.addAll(node.layoutNodes)
|
||||
parentNode.mergedSemantics.addAll(node.mergedSemantics)
|
||||
parentNode.unmergedSemantics.addAll(node.unmergedSemantics)
|
||||
release(node)
|
||||
}
|
||||
val nodeId = id
|
||||
parentNode.id = if (parentNode.id <= UNDEFINED_ID && nodeId != null) nodeId else parentNode.id
|
||||
}
|
||||
|
||||
@OptIn(InternalComposeUiApi::class)
|
||||
private fun parse(
|
||||
group: CompositionGroup,
|
||||
context: SourceContext,
|
||||
children: List<MutableInspectorNode>
|
||||
): MutableInspectorNode {
|
||||
val node = newNode()
|
||||
node.name = context.name ?: ""
|
||||
node.key = group.key as? Int ?: 0
|
||||
node.inlined = context.isInline
|
||||
|
||||
// If this node is associated with an android View, set the node's viewId to point to
|
||||
// the hosted view. We use the parent's uniqueDrawingId since the interopView returned here
|
||||
// will be the view itself, but we want to use the `AndroidViewHolder` that hosts the view
|
||||
// instead of the view directly.
|
||||
(group.node as? InteroperableComposeUiNode?)?.getInteropView()?.let { interopView ->
|
||||
(interopView.parent as? View)?.uniqueDrawingId?.let { viewId -> node.viewId = viewId }
|
||||
}
|
||||
|
||||
val layoutInfo = group.node as? LayoutInfo
|
||||
if (layoutInfo != null) {
|
||||
return parseLayoutInfo(layoutInfo, context, node)
|
||||
}
|
||||
if (unwantedOutsideWindow(node, children)) {
|
||||
return markUnwanted(group, context, node)
|
||||
}
|
||||
node.box = context.bounds.emptyCheck()
|
||||
if (unwantedName(node.name) || (node.box == emptyBox && !subCompositions.capturing)) {
|
||||
return markUnwanted(group, context, node)
|
||||
}
|
||||
parseCallLocation(node, context.location)
|
||||
if (isHiddenSystemNode(node)) {
|
||||
return markUnwanted(group, context, node)
|
||||
}
|
||||
node.anchorId = anchorMap[group.identity]
|
||||
node.id = syntheticId(node.anchorId)
|
||||
if (includeAllParameters) {
|
||||
addParameters(context, node)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
private fun IntRect.emptyCheck(): IntRect = if (left >= right && top >= bottom) emptyBox else this
|
||||
|
||||
private fun IntRect.inWindow(): Boolean =
|
||||
!(left > windowSize.width || right < 0 || top > windowSize.height || bottom < 0)
|
||||
|
||||
private fun IntRect.union(other: IntRect): IntRect {
|
||||
if (this == outsideBox) return other else if (other == outsideBox) return this
|
||||
|
||||
return IntRect(
|
||||
left = min(left, other.left),
|
||||
top = min(top, other.top),
|
||||
bottom = max(bottom, other.bottom),
|
||||
right = max(right, other.right))
|
||||
}
|
||||
|
||||
private fun parseLayoutInfo(
|
||||
layoutInfo: LayoutInfo,
|
||||
context: SourceContext,
|
||||
node: MutableInspectorNode
|
||||
): MutableInspectorNode {
|
||||
val box = context.bounds
|
||||
val size = box.size.toSize()
|
||||
val coordinates = layoutInfo.coordinates
|
||||
val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
|
||||
val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
|
||||
val bottomRight = toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
|
||||
val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
|
||||
var bounds: QuadBounds? = null
|
||||
|
||||
if (topLeft.x != box.left ||
|
||||
topLeft.y != box.top ||
|
||||
topRight.x != box.right ||
|
||||
topRight.y != box.top ||
|
||||
bottomRight.x != box.right ||
|
||||
bottomRight.y != box.bottom ||
|
||||
bottomLeft.x != box.left ||
|
||||
bottomLeft.y != box.bottom) {
|
||||
bounds =
|
||||
QuadBounds(
|
||||
topLeft.x,
|
||||
topLeft.y,
|
||||
topRight.x,
|
||||
topRight.y,
|
||||
bottomRight.x,
|
||||
bottomRight.y,
|
||||
bottomLeft.x,
|
||||
bottomLeft.y,
|
||||
)
|
||||
}
|
||||
if (!includeNodesOutsizeOfWindow) {
|
||||
// Ignore this node if the bounds are completely outside the window
|
||||
node.outerBox = bounds?.outerBox ?: box
|
||||
if (!node.outerBox.inWindow()) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
node.box = box.emptyCheck()
|
||||
node.bounds = bounds
|
||||
node.layoutNodes.add(layoutInfo)
|
||||
val modifierInfo = layoutInfo.getModifierInfo()
|
||||
|
||||
val unmergedSemantics = unmergedSemanticsMap[layoutInfo.semanticsId]
|
||||
if (unmergedSemantics != null) {
|
||||
node.unmergedSemantics.addAll(unmergedSemantics)
|
||||
}
|
||||
|
||||
val mergedSemantics = semanticsMap[layoutInfo.semanticsId]
|
||||
if (mergedSemantics != null) {
|
||||
node.mergedSemantics.addAll(mergedSemantics)
|
||||
}
|
||||
|
||||
node.id =
|
||||
modifierInfo
|
||||
.asSequence()
|
||||
.map { it.extra }
|
||||
.filterIsInstance<GraphicLayerInfo>()
|
||||
.map { it.layerId }
|
||||
.firstOrNull()
|
||||
?: UNDEFINED_ID
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
private fun syntheticId(anchorId: Int): Long {
|
||||
if (anchorId == NO_ANCHOR_ID) {
|
||||
return UNDEFINED_ID
|
||||
}
|
||||
// The anchorId is an Int
|
||||
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)
|
||||
|
||||
private fun addParameters(context: SourceContext, node: MutableInspectorNode) {
|
||||
context.parameters.forEach {
|
||||
val castedValue = castValue(it)
|
||||
node.parameters.add(RawParameter(it.name, castedValue))
|
||||
}
|
||||
}
|
||||
|
||||
private fun castValue(parameter: ParameterInformation): Any? {
|
||||
val value = parameter.value ?: return null
|
||||
if (parameter.inlineClass == null || !isPrimitive(value.javaClass)) return value
|
||||
return inlineClassConverter.castParameterValue(parameter.inlineClass, value)
|
||||
}
|
||||
|
||||
private fun isPrimitive(cls: Class<*>): Boolean = cls.kotlin.javaPrimitiveType != null
|
||||
|
||||
private fun toIntOffset(offset: Offset): IntOffset =
|
||||
IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
|
||||
|
||||
private fun markUnwanted(
|
||||
group: CompositionGroup,
|
||||
context: SourceContext,
|
||||
node: MutableInspectorNode
|
||||
): MutableInspectorNode =
|
||||
when (node.name) {
|
||||
"rememberCompositionContext" -> subCompositions.rememberCompositionContext(node, context)
|
||||
"remember" -> subCompositions.remember(node, group)
|
||||
else -> node.apply { markUnwanted() }
|
||||
}
|
||||
|
||||
private fun parseCallLocation(node: MutableInspectorNode, location: SourceLocation?) {
|
||||
val fileName = location?.sourceFile ?: return
|
||||
node.fileName = fileName
|
||||
node.packageHash = location.packageHash
|
||||
node.lineNumber = location.lineNumber
|
||||
node.offset = location.offset
|
||||
node.length = location.length
|
||||
}
|
||||
|
||||
private fun isHiddenSystemNode(node: MutableInspectorNode): Boolean =
|
||||
node.packageHash in systemPackages && hideSystemNodes
|
||||
|
||||
private fun unwantedName(name: String): Boolean =
|
||||
name.isEmpty() || name.startsWith("remember") || name in unwantedCalls
|
||||
|
||||
private fun unwantedOutsideWindow(
|
||||
node: MutableInspectorNode,
|
||||
children: List<MutableInspectorNode>
|
||||
): Boolean {
|
||||
if (includeNodesOutsizeOfWindow) {
|
||||
return false
|
||||
}
|
||||
node.outerBox =
|
||||
if (children.isEmpty()) outsideBox
|
||||
else children.map { g -> g.outerBox }.reduce { acc, box -> box.union(acc) }
|
||||
return !node.outerBox.inWindow()
|
||||
}
|
||||
|
||||
private fun newNode(): MutableInspectorNode =
|
||||
if (cache.isNotEmpty()) cache.pop() else MutableInspectorNode()
|
||||
|
||||
private fun newNode(copyFrom: InspectorNode): MutableInspectorNode =
|
||||
newNode().shallowCopy(copyFrom)
|
||||
|
||||
private fun release(node: MutableInspectorNode) {
|
||||
node.reset()
|
||||
cache.add(node)
|
||||
}
|
||||
|
||||
private fun buildAndRelease(node: MutableInspectorNode): InspectorNode {
|
||||
val result = node.build()
|
||||
release(node)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of sub-composition roots.
|
||||
*
|
||||
* Examples:
|
||||
* - Popup, Dialog: When one of these is open an extra Android Window is created with its own
|
||||
* AndroidComposeView. The contents of the Composable is a sub-composition that will be computed
|
||||
* by calling convert.
|
||||
*
|
||||
* The Popup/Dialog composable itself, and a few helping composables (the root) will not be
|
||||
* included in the SlotTree with the contents, instead these composables will be found in the
|
||||
* SlotTree for the main app and they all have empty sizes. The aim is to collect these
|
||||
* sub-composition roots such that they can be added to the [InspectorNode]s of the contents.
|
||||
* - AndroidView: When this is used in a compose app we will see a similar pattern in the SlotTree
|
||||
* except there isn't a sub-composition to stitch in. But we need to collect the view id
|
||||
* separately from the "AndroidView" node itself.
|
||||
*/
|
||||
private inner class SubCompositionRoots {
|
||||
/** Set to true when the nodes found should be added to a sub-composition root */
|
||||
var capturing = false
|
||||
private set
|
||||
|
||||
/** The `uniqueDrawingId` of the `AndroidComposeView` that owns the root being captured */
|
||||
private var ownerView = UNDEFINED_ID
|
||||
|
||||
/** The node that represent the root of the sub-composition */
|
||||
private var rootNode: MutableInspectorNode? = null
|
||||
|
||||
/** The depth of the parse tree the [rootNode] was found at */
|
||||
private var rootNodeDepth = 0
|
||||
|
||||
/** Last captured view that is believed to be an embbed View under an AndroidView node */
|
||||
private var androidView = UNDEFINED_ID
|
||||
|
||||
/**
|
||||
* The sub-composition roots found.
|
||||
*
|
||||
* 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>()
|
||||
|
||||
/** Call this before converting a SlotTree for an AndroidComposeView */
|
||||
fun clear() {
|
||||
capturing = false
|
||||
ownerView = UNDEFINED_ID
|
||||
rootNode?.markUnwanted()
|
||||
rootNode?.id = UNDEFINED_ID
|
||||
rootNode = null
|
||||
rootNodeDepth = 0
|
||||
}
|
||||
|
||||
/** Call this when starting converting a new set of windows */
|
||||
fun resetAccumulativeState() {
|
||||
found.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* When a "rememberCompositionContext" is found in the slot tree, it indicates that a
|
||||
* sub-composition was started. We should capture all parent nodes with an empty size as the
|
||||
* "root" of the sub-composition.
|
||||
*/
|
||||
fun rememberCompositionContext(
|
||||
node: MutableInspectorNode,
|
||||
context: SourceContext
|
||||
): MutableInspectorNode {
|
||||
if (capturing) {
|
||||
save()
|
||||
}
|
||||
capturing = true
|
||||
rootNode = node
|
||||
rootNodeDepth = context.depth
|
||||
node.id = PLACEHOLDER_ID
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* When "remember" is found in the slot tree and we are currently capturing, the data of the
|
||||
* [group] may contain the owner of the sub-composition.
|
||||
*/
|
||||
fun remember(node: MutableInspectorNode, group: CompositionGroup): MutableInspectorNode {
|
||||
node.markUnwanted()
|
||||
if (!capturing) {
|
||||
return node
|
||||
}
|
||||
val root = findSingleRootInGroupData(group) ?: return node
|
||||
|
||||
val view = root.subCompositionView
|
||||
if (view != null) {
|
||||
val composeOwner = if (view.childCount == 1) view.getChildAt(0) else return node
|
||||
ownerView = composeOwner.uniqueDrawingId
|
||||
} else {
|
||||
androidView = root.viewRoot?.uniqueDrawingId ?: UNDEFINED_ID
|
||||
// Store the viewRoot such that we can move the View under the compose node
|
||||
// in Studio. We do not need to capture the Groups found for this case, so
|
||||
// we call "reset" here to stop capturing.
|
||||
clear()
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
private fun findSingleRootInGroupData(group: CompositionGroup): ViewRootForInspector? {
|
||||
group.data.filterIsInstance<ViewRootForInspector>().singleOrNull()?.let {
|
||||
return it
|
||||
}
|
||||
val refs = group.data.filterIsInstance<Ref<*>>().map { it.value }
|
||||
return refs.filterIsInstance<ViewRootForInspector>().singleOrNull()
|
||||
}
|
||||
|
||||
/** Capture the top node of the sub-composition root until a non empty node is found. */
|
||||
fun captureNode(node: MutableInspectorNode, context: SourceContext) {
|
||||
if (!capturing) {
|
||||
return
|
||||
}
|
||||
if (node.box != emptyBox) {
|
||||
save()
|
||||
return
|
||||
}
|
||||
val depth = context.depth
|
||||
if (depth < rootNodeDepth) {
|
||||
rootNode = node
|
||||
rootNodeDepth = depth
|
||||
}
|
||||
}
|
||||
|
||||
fun latestViewId(): Long {
|
||||
val id = androidView
|
||||
androidView = UNDEFINED_ID
|
||||
return id
|
||||
}
|
||||
|
||||
/** If a sub-composition root has been captured, save it now. */
|
||||
private fun save() {
|
||||
val node = rootNode
|
||||
if (node != null && ownerView != UNDEFINED_ID) {
|
||||
found[ownerView] = node.build()
|
||||
}
|
||||
node?.markUnwanted()
|
||||
node?.id = UNDEFINED_ID
|
||||
node?.children?.clear()
|
||||
clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the root of the sub-composition to the found tree.
|
||||
*
|
||||
* If a root is not found for this [owner] or if the stitching fails just return [nodes].
|
||||
*/
|
||||
fun addRoot(owner: View, nodes: List<InspectorNode>): List<InspectorNode> {
|
||||
val root = found[owner.uniqueDrawingId] ?: return nodes
|
||||
val box = IntRect(0, 0, owner.width, owner.height)
|
||||
val info = StitchInfo(nodes, box)
|
||||
val result = listOf(stitch(root, info))
|
||||
return if (info.added) result else nodes
|
||||
}
|
||||
|
||||
private fun stitch(node: InspectorNode, info: StitchInfo): InspectorNode {
|
||||
val children = node.children.map { stitch(it, info) }
|
||||
val index = children.indexOfFirst { it.id == PLACEHOLDER_ID }
|
||||
val newNode = newNode()
|
||||
newNode.shallowCopy(node)
|
||||
newNode.children.clear()
|
||||
if (index < 0) {
|
||||
newNode.children.addAll(children)
|
||||
} else {
|
||||
newNode.children.addAll(children.subList(0, index))
|
||||
newNode.children.addAll(info.nodes)
|
||||
newNode.children.addAll(children.subList(index + 1, children.size))
|
||||
info.added = true
|
||||
}
|
||||
newNode.box = info.bounds
|
||||
return buildAndRelease(newNode)
|
||||
}
|
||||
}
|
||||
|
||||
private class StitchInfo(
|
||||
/** The nodes found that should be stitched into a sub-composition root. */
|
||||
val nodes: List<InspectorNode>,
|
||||
|
||||
/** The bounds of the View containing the sub-composition */
|
||||
val bounds: IntRect
|
||||
) {
|
||||
/** Set this to true when the [nodes] have been added to a sub-composition root */
|
||||
var added: Boolean = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/** Holds data representing a Composable parameter for the Layout Inspector. */
|
||||
class NodeParameter
|
||||
internal constructor(
|
||||
/** The name of the parameter. */
|
||||
val name: String,
|
||||
|
||||
/** The type of the parameter. */
|
||||
val type: ParameterType,
|
||||
|
||||
/** The value of the parameter. */
|
||||
val value: Any?
|
||||
) {
|
||||
/** Sub elements of the parameter. */
|
||||
val elements = mutableListOf<NodeParameter>()
|
||||
|
||||
/** Reference to value parameter. */
|
||||
var reference: NodeParameterReference? = null
|
||||
|
||||
/** The index into the composite parent parameter value. */
|
||||
var index = 0
|
||||
}
|
||||
|
||||
/** The type of a parameter. */
|
||||
enum class ParameterType {
|
||||
String,
|
||||
Boolean,
|
||||
Double,
|
||||
Float,
|
||||
Int32,
|
||||
Int64,
|
||||
Color,
|
||||
Resource,
|
||||
DimensionDp,
|
||||
DimensionSp,
|
||||
DimensionEm,
|
||||
Lambda,
|
||||
FunctionReference,
|
||||
Iterable,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 facebook.internal.androidx.compose.ui.inspection.inspector
|
||||
|
||||
import facebook.internal.androidx.compose.ui.inspection.util.asIntArray
|
||||
|
||||
/**
|
||||
* A reference to a parameter to a [NodeParameter]
|
||||
*
|
||||
* @param nodeId is the id of the node the parameter belongs to
|
||||
* @param anchorId is the anchor hash of the node the parameter belongs to
|
||||
* @param kind is this a reference to a normal, merged, or unmerged semantic parameter.
|
||||
* @param parameterIndex index into [InspectorNode.parameters], [InspectorNode.mergedSemantics], or
|
||||
* [InspectorNode.unMergedSemantics]
|
||||
* @param indices are indices into the composite parameter
|
||||
*/
|
||||
class NodeParameterReference(
|
||||
val nodeId: Long,
|
||||
val anchorId: Int,
|
||||
val kind: ParameterKind,
|
||||
val parameterIndex: Int,
|
||||
val indices: IntArray
|
||||
) {
|
||||
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 ""
|
||||
return "[$nodeId, $anchorId, $kind, $parameterIndex$suffix]"
|
||||
}
|
||||
}
|
||||
|
||||
/** Identifies which kind of parameter the [NodeParameterReference] is a reference to. */
|
||||
enum class ParameterKind {
|
||||
Normal,
|
||||
MergedSemantics,
|
||||
UnmergedSemantics
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// WARNING: DO NOT EDIT THIS FILE MANUALLY. It's automatically generated by running:
|
||||
// frameworks/support/compose/ui/ui-inspection/generate-packages/generate_compose_packages.py -r
|
||||
package facebook.internal.androidx.compose.ui.inspection.inspector
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@VisibleForTesting
|
||||
fun packageNameHash(packageName: String) =
|
||||
packageName.fold(0) { hash, char -> hash * 31 + char.code }.absoluteValue
|
||||
|
||||
val systemPackages =
|
||||
setOf(
|
||||
-1,
|
||||
packageNameHash("androidx.compose.animation"),
|
||||
packageNameHash("androidx.compose.animation.core"),
|
||||
packageNameHash("androidx.compose.animation.graphics.vector"),
|
||||
packageNameHash("androidx.compose.desktop"),
|
||||
packageNameHash("androidx.compose.foundation"),
|
||||
packageNameHash("androidx.compose.foundation.gestures"),
|
||||
packageNameHash("androidx.compose.foundation.gestures.snapping"),
|
||||
packageNameHash("androidx.compose.foundation.interaction"),
|
||||
packageNameHash("androidx.compose.foundation.layout"),
|
||||
packageNameHash("androidx.compose.foundation.lazy"),
|
||||
packageNameHash("androidx.compose.foundation.lazy.grid"),
|
||||
packageNameHash("androidx.compose.foundation.lazy.layout"),
|
||||
packageNameHash("androidx.compose.foundation.lazy.staggeredgrid"),
|
||||
packageNameHash("androidx.compose.foundation.newtext.text"),
|
||||
packageNameHash("androidx.compose.foundation.newtext.text.copypasta"),
|
||||
packageNameHash("androidx.compose.foundation.newtext.text.copypasta.selection"),
|
||||
packageNameHash("androidx.compose.foundation.pager"),
|
||||
packageNameHash("androidx.compose.foundation.relocation"),
|
||||
packageNameHash("androidx.compose.foundation.text"),
|
||||
packageNameHash("androidx.compose.foundation.text.selection"),
|
||||
packageNameHash("androidx.compose.foundation.window"),
|
||||
packageNameHash("androidx.compose.material"),
|
||||
packageNameHash("androidx.compose.material.internal"),
|
||||
packageNameHash("androidx.compose.material.pullrefresh"),
|
||||
packageNameHash("androidx.compose.material.ripple"),
|
||||
packageNameHash("androidx.compose.material3"),
|
||||
packageNameHash("androidx.compose.material3.internal"),
|
||||
packageNameHash("androidx.compose.material3.windowsizeclass"),
|
||||
packageNameHash("androidx.compose.runtime"),
|
||||
packageNameHash("androidx.compose.runtime.livedata"),
|
||||
packageNameHash("androidx.compose.runtime.mock"),
|
||||
packageNameHash("androidx.compose.runtime.reflect"),
|
||||
packageNameHash("androidx.compose.runtime.rxjava2"),
|
||||
packageNameHash("androidx.compose.runtime.rxjava3"),
|
||||
packageNameHash("androidx.compose.runtime.saveable"),
|
||||
packageNameHash("androidx.compose.ui"),
|
||||
packageNameHash("androidx.compose.ui.awt"),
|
||||
packageNameHash("androidx.compose.ui.graphics.benchmark"),
|
||||
packageNameHash("androidx.compose.ui.graphics.vector"),
|
||||
packageNameHash("androidx.compose.ui.layout"),
|
||||
packageNameHash("androidx.compose.ui.platform"),
|
||||
packageNameHash("androidx.compose.ui.text"),
|
||||
packageNameHash("androidx.compose.ui.util"),
|
||||
packageNameHash("androidx.compose.ui.viewinterop"),
|
||||
packageNameHash("androidx.compose.ui.window"),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 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 <T> 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<ReflectionFactory>()
|
||||
|
||||
fun <T> 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<KTypeProjection>?,
|
||||
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<KType>?) {
|
||||
factory.setUpperBounds(typeParameter, bounds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2022 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.util
|
||||
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
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>()
|
||||
|
||||
/** Return a unique id for the specified [anchor] instance. */
|
||||
operator fun get(anchor: Any?): Int =
|
||||
anchor?.let { idLookup.getOrPut(it) { generateUniqueId(it) } } ?: NO_ANCHOR_ID
|
||||
|
||||
/** Return the anchor associated with a given unique anchor [id]. */
|
||||
operator fun get(id: Int): Any? = anchorLookup[id]
|
||||
|
||||
private fun generateUniqueId(anchor: Any): Int {
|
||||
var id = anchor.hashCode()
|
||||
while (id == NO_ANCHOR_ID || anchorLookup.containsKey(id)) {
|
||||
id++
|
||||
}
|
||||
anchorLookup[id] = anchor
|
||||
return id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 facebook.internal.androidx.compose.ui.inspection.util
|
||||
|
||||
private val EMPTY_INT_ARRAY = intArrayOf()
|
||||
|
||||
fun List<Int>.asIntArray() = if (isNotEmpty()) toIntArray() else EMPTY_INT_ARRAY
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 facebook.internal.androidx.compose.ui.inspection.util
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Future
|
||||
|
||||
object ThreadUtils {
|
||||
fun assertOnMainThread() {
|
||||
if (!Looper.getMainLooper().isCurrentThread) {
|
||||
error("This work is required on the main thread")
|
||||
}
|
||||
}
|
||||
|
||||
fun assertOffMainThread() {
|
||||
if (Looper.getMainLooper().isCurrentThread) {
|
||||
error("This work is required off the main thread")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run some logic on the main thread, returning a future that will contain any data computed by
|
||||
* and returned from the block.
|
||||
*
|
||||
* If this method is called from the main thread, it will run immediately.
|
||||
*/
|
||||
fun <T> runOnMainThread(block: () -> T): Future<T> {
|
||||
return if (!Looper.getMainLooper().isCurrentThread) {
|
||||
val future = CompletableFuture<T>()
|
||||
Handler.createAsync(Looper.getMainLooper()).post { future.complete(block()) }
|
||||
future
|
||||
} else {
|
||||
CompletableFuture.completedFuture(block())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user