Merge pull request #4885 from facebook/fixup-T156813884-main

Re-sync with internal repository
This commit is contained in:
Pascal Hartig
2023-06-27 10:07:41 +01:00
committed by GitHub
12 changed files with 2636 additions and 0 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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())
}

View File

@@ -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
}
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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"),
)

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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())
}
}
}