Flatten layout during traversal
Summary: Move from a nested structure to a flatten one for data exchange, this will allow us to only send sections of the UI in the future Reviewed By: lblasa Differential Revision: D38982138 fbshipit-source-id: d578a07a6d2d7e117fbd741bd6e33062223ce10d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
55b852f90c
commit
a5da6923eb
@@ -8,12 +8,14 @@
|
||||
package com.facebook.flipper.plugins.uidebugger
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.facebook.flipper.core.FlipperConnection
|
||||
import com.facebook.flipper.core.FlipperPlugin
|
||||
import com.facebook.flipper.plugins.uidebugger.common.Node
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationInspector
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
|
||||
import com.facebook.flipper.plugins.uidebugger.core.Context
|
||||
import com.facebook.flipper.plugins.uidebugger.model.InitEvent
|
||||
import com.facebook.flipper.plugins.uidebugger.model.NativeScanEvent
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
val LogTag = "FlipperUIDebugger"
|
||||
@@ -32,17 +34,22 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
|
||||
this.connection = connection
|
||||
// temp solution, get from descriptor
|
||||
val inspector = ApplicationInspector(context)
|
||||
val root: Node = inspector.inspect()!!
|
||||
val initEvent = InitEvent(System.identityHashCode(application).toString())
|
||||
|
||||
val rootDescriptor =
|
||||
inspector.descriptorRegister.descriptorForClassUnsafe(context.applicationRef.javaClass)
|
||||
connection.send(
|
||||
InitEvent.name,
|
||||
Json.encodeToString(
|
||||
InitEvent.serializer(), InitEvent(System.identityHashCode(application).toString())))
|
||||
InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef))))
|
||||
|
||||
connection.send(
|
||||
NativeScanEvent.name,
|
||||
Json.encodeToString(NativeScanEvent.serializer(), NativeScanEvent(root)))
|
||||
try {
|
||||
val nodes = inspector.traversal.traverse()
|
||||
connection.send(
|
||||
NativeScanEvent.name,
|
||||
Json.encodeToString(NativeScanEvent.serializer(), NativeScanEvent(nodes)))
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(LogTag, e.message.toString(), e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.uidebugger.common
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class Node() {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var attributes: Map<String, InspectableObject> = mapOf()
|
||||
var children: List<Node>? = null
|
||||
}
|
||||
@@ -9,16 +9,11 @@ package com.facebook.flipper.plugins.uidebugger.core
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import com.facebook.flipper.plugins.uidebugger.common.Node
|
||||
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
|
||||
|
||||
class ApplicationInspector(val context: Context) {
|
||||
val descriptorRegister = DescriptorRegister.withDefaults()
|
||||
val traversal = LayoutTraversal(descriptorRegister)
|
||||
|
||||
fun inspect(): Node? {
|
||||
return traversal.inspect(context.application)
|
||||
}
|
||||
val traversal = LayoutTraversal(descriptorRegister, context.applicationRef)
|
||||
|
||||
fun attachListeners(view: View) {
|
||||
// An OnGlobalLayoutListener watches the entire hierarchy for layout changes
|
||||
@@ -49,7 +44,6 @@ class ApplicationInspector(val context: Context) {
|
||||
}
|
||||
|
||||
override fun onRootViewRemoved(view: View) {}
|
||||
|
||||
override fun onRootViewsChanged(views: java.util.List<View>) {}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
|
||||
package com.facebook.flipper.plugins.uidebugger.core
|
||||
|
||||
class Context(val application: ApplicationRef) {}
|
||||
class Context(val applicationRef: ApplicationRef) {}
|
||||
|
||||
@@ -7,63 +7,57 @@
|
||||
|
||||
package com.facebook.flipper.plugins.uidebugger.core
|
||||
|
||||
import android.util.Log
|
||||
import com.facebook.flipper.plugins.uidebugger.LogTag
|
||||
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
|
||||
import com.facebook.flipper.plugins.uidebugger.common.Node
|
||||
import com.facebook.flipper.plugins.uidebugger.descriptors.Descriptor
|
||||
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
|
||||
import com.facebook.flipper.plugins.uidebugger.model.Node
|
||||
|
||||
class LayoutTraversal(private val descriptorRegister: DescriptorRegister) {
|
||||
class IntermediateNode(val node: Node) {
|
||||
var children: List<Any>? = null
|
||||
}
|
||||
class LayoutTraversal(
|
||||
private val descriptorRegister: DescriptorRegister,
|
||||
val root: ApplicationRef
|
||||
) {
|
||||
|
||||
internal inline fun Descriptor<*>.asAny(): Descriptor<Any> = this as Descriptor<Any>
|
||||
|
||||
private fun describe(obj: Any): IntermediateNode {
|
||||
var intermediate = IntermediateNode(Node())
|
||||
/** Traverses the native android hierarchy */
|
||||
fun traverse(): List<Node> {
|
||||
|
||||
val descriptor = descriptorRegister.descriptorForClass(obj::class.java)
|
||||
descriptor?.let { descriptor ->
|
||||
val anyDescriptor = descriptor.asAny()
|
||||
val result = mutableListOf<Node>()
|
||||
val stack = mutableListOf<Any>()
|
||||
stack.add(this.root)
|
||||
|
||||
intermediate.node.id = anyDescriptor.getId(obj)
|
||||
intermediate.node.name = anyDescriptor.getName(obj)
|
||||
while (stack.isNotEmpty()) {
|
||||
|
||||
val attributes = mutableMapOf<String, InspectableObject>()
|
||||
anyDescriptor.getData(obj, attributes)
|
||||
intermediate.node.attributes = attributes
|
||||
val node = stack.removeLast()
|
||||
|
||||
val children = mutableListOf<Any>()
|
||||
anyDescriptor.getChildren(obj, children)
|
||||
intermediate.children = children
|
||||
}
|
||||
try {
|
||||
|
||||
return intermediate
|
||||
}
|
||||
val descriptor = descriptorRegister.descriptorForClassUnsafe(node::class.java).asAny()
|
||||
|
||||
private fun traverse(entry: Any): Node? {
|
||||
val root = describe(entry)
|
||||
root?.let { intermediate ->
|
||||
val queue = mutableListOf<IntermediateNode>()
|
||||
queue.add(intermediate)
|
||||
val children = mutableListOf<Any>()
|
||||
descriptor.getChildren(node, children)
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val intermediateNode = queue.removeFirst()
|
||||
|
||||
val children = mutableListOf<Node>()
|
||||
intermediateNode.children?.forEach {
|
||||
val intermediateChild = describe(it)
|
||||
children.add(intermediateChild.node)
|
||||
queue.add(intermediateChild)
|
||||
val childrenIds = mutableListOf<String>()
|
||||
for (child in children) {
|
||||
// it might make sense one day to remove id from the descriptor since its always the
|
||||
// hash code
|
||||
val childDescriptor =
|
||||
descriptorRegister.descriptorForClassUnsafe(child::class.java).asAny()
|
||||
childrenIds.add(childDescriptor.getId(child))
|
||||
stack.add(child)
|
||||
}
|
||||
intermediateNode.node.children = children
|
||||
|
||||
val attributes = mutableMapOf<String, InspectableObject>()
|
||||
descriptor.getData(node, attributes)
|
||||
|
||||
result.add(Node(descriptor.getId(node), descriptor.getName(node), attributes, childrenIds))
|
||||
} catch (exception: Exception) {
|
||||
Log.e(LogTag, "Error while processing node ${node.javaClass.name} ${node} ", exception)
|
||||
}
|
||||
}
|
||||
|
||||
return root?.node
|
||||
}
|
||||
|
||||
fun inspect(applicationRef: ApplicationRef): Node? {
|
||||
return traverse(applicationRef)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
|
||||
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
|
||||
|
||||
class ApplicationDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
|
||||
class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
|
||||
val rootResolver = RootViewResolver()
|
||||
|
||||
override fun onInit() {}
|
||||
@@ -13,6 +13,7 @@ import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import com.facebook.flipper.plugins.uidebugger.common.UIDebuggerException
|
||||
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
|
||||
|
||||
class DescriptorRegister {
|
||||
@@ -23,7 +24,7 @@ class DescriptorRegister {
|
||||
fun withDefaults(): DescriptorRegister {
|
||||
val mapping = DescriptorRegister()
|
||||
mapping.register(Any::class.java, ObjectDescriptor())
|
||||
mapping.register(ApplicationRef::class.java, ApplicationDescriptor())
|
||||
mapping.register(ApplicationRef::class.java, ApplicationRefDescriptor())
|
||||
mapping.register(Activity::class.java, ActivityDescriptor())
|
||||
mapping.register(Window::class.java, WindowDescriptor())
|
||||
mapping.register(ViewGroup::class.java, ViewGroupDescriptor())
|
||||
@@ -57,11 +58,16 @@ class DescriptorRegister {
|
||||
register[clazz] = descriptor
|
||||
}
|
||||
|
||||
fun descriptorForClass(clazz: Class<*>): Descriptor<*>? {
|
||||
var clazz = clazz
|
||||
fun <T> descriptorForClass(clazz: Class<T>): Descriptor<T>? {
|
||||
var clazz: Class<*> = clazz
|
||||
while (!register.containsKey(clazz)) {
|
||||
clazz = clazz.superclass
|
||||
}
|
||||
return register[clazz]
|
||||
return register[clazz] as Descriptor<T>
|
||||
}
|
||||
|
||||
fun <T> descriptorForClassUnsafe(clazz: Class<T>): Descriptor<T> {
|
||||
return descriptorForClass(clazz)
|
||||
?: throw UIDebuggerException("No descriptor found for ${clazz.name}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,18 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.uidebugger
|
||||
|
||||
import com.facebook.flipper.plugins.uidebugger.common.Node
|
||||
package com.facebook.flipper.plugins.uidebugger.model
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class InitEvent(val rootId: String) {
|
||||
companion object {
|
||||
val name = "init"
|
||||
const val name = "init"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO flatten the tree into normalised list
|
||||
@kotlinx.serialization.Serializable
|
||||
data class NativeScanEvent(val root: Node) {
|
||||
data class NativeScanEvent(val nodes: List<Node>) {
|
||||
companion object {
|
||||
val name = "nativeScan"
|
||||
const val name = "nativeScan"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.uidebugger.model
|
||||
|
||||
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class Node(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val attributes: Map<String, InspectableObject>,
|
||||
val children: List<String>
|
||||
)
|
||||
@@ -14,43 +14,57 @@ import {Tree} from 'antd';
|
||||
import type {DataNode} from 'antd/es/tree';
|
||||
import {DownOutlined} from '@ant-design/icons';
|
||||
|
||||
function treeToAntTree(uiNode: UINode): DataNode {
|
||||
return {
|
||||
key: uiNode.id,
|
||||
title: uiNode.name,
|
||||
children: uiNode.children ? uiNode.children.map(treeToAntTree) : [],
|
||||
};
|
||||
}
|
||||
// function treeToAntTree(uiNode: UINode): DataNode {
|
||||
// return {
|
||||
// key: uiNode.id,
|
||||
// title: uiNode.name,
|
||||
// children: uiNode.children ? uiNode.children.map(treeToAntTree) : [],
|
||||
// };
|
||||
// }
|
||||
|
||||
function treeToMap(uiNode: UINode): Map<Id, UINode> {
|
||||
const result = new Map<Id, UINode>();
|
||||
// function treeToMap(uiNode: UINode): Map<Id, UINode> {
|
||||
// const result = new Map<Id, UINode>();
|
||||
//
|
||||
// function treeToMapRec(node: UINode): void {
|
||||
// result.set(node.id, node);
|
||||
// for (const child of node.children) {
|
||||
// treeToMapRec(child);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// treeToMapRec(uiNode);
|
||||
//
|
||||
// return result;
|
||||
// }
|
||||
|
||||
function treeToMapRec(node: UINode): void {
|
||||
result.set(node.id, node);
|
||||
for (const child of node.children) {
|
||||
treeToMapRec(child);
|
||||
}
|
||||
function nodesToAntTree(root: Id, nodes: Map<Id, UINode>): DataNode {
|
||||
function uiNodeToAntNode(id: Id): DataNode {
|
||||
const node = nodes.get(id);
|
||||
return {
|
||||
key: id,
|
||||
title: node?.name,
|
||||
children: node?.children.map((id) => uiNodeToAntNode(id)),
|
||||
};
|
||||
}
|
||||
|
||||
treeToMapRec(uiNode);
|
||||
|
||||
return result;
|
||||
return uiNodeToAntNode(root);
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const instance = usePlugin(plugin);
|
||||
const rootId = useValue(instance.rootId);
|
||||
const tree = useValue(instance.tree);
|
||||
const nodes = useValue(instance.nodes);
|
||||
|
||||
if (tree) {
|
||||
const nodeMap = treeToMap(tree);
|
||||
const antTree = treeToAntTree(tree);
|
||||
if (rootId) {
|
||||
const antTree = nodesToAntTree(rootId, nodes);
|
||||
console.log(antTree);
|
||||
console.log(rootId);
|
||||
return (
|
||||
<Tree
|
||||
showIcon
|
||||
showLine
|
||||
onSelect={(selected) => {
|
||||
console.log(nodeMap.get(selected[0] as string));
|
||||
console.log(nodes.get(selected[0] as string));
|
||||
}}
|
||||
defaultExpandAll
|
||||
switcherIcon={<DownOutlined />}
|
||||
@@ -59,5 +73,5 @@ export function Component() {
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{rootId}</div>;
|
||||
return <div>Nothing yet</div>;
|
||||
}
|
||||
|
||||
@@ -43,26 +43,26 @@ export type UINode = {
|
||||
id: Id;
|
||||
name: string;
|
||||
attributes: Record<string, Inspectable>;
|
||||
children: UINode[];
|
||||
children: Id[];
|
||||
};
|
||||
|
||||
type Events = {
|
||||
init: {rootId: string};
|
||||
|
||||
nativeScan: {root: UINode};
|
||||
nativeScan: {nodes: UINode[]};
|
||||
};
|
||||
|
||||
export function plugin(client: PluginClient<Events>) {
|
||||
const rootId = createState<string | undefined>(undefined);
|
||||
const rootId = createState<Id | undefined>(undefined);
|
||||
|
||||
const tree = createState<UINode | undefined>(undefined);
|
||||
const nodesAtom = createState<Map<Id, UINode>>(new Map());
|
||||
client.onMessage('init', (root) => rootId.set(root.rootId));
|
||||
|
||||
client.onMessage('nativeScan', ({root}) => {
|
||||
tree.set(root as UINode);
|
||||
client.onMessage('nativeScan', ({nodes}) => {
|
||||
nodesAtom.set(new Map(nodes.map((node) => [node.id, node])));
|
||||
console.log(nodesAtom.get());
|
||||
});
|
||||
|
||||
return {rootId, tree};
|
||||
return {rootId, nodes: nodesAtom};
|
||||
}
|
||||
|
||||
export {Component} from './components/main';
|
||||
|
||||
Reference in New Issue
Block a user