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:
Luke De Feo
2022-09-07 04:37:17 -07:00
committed by Facebook GitHub Bot
parent 55b852f90c
commit a5da6923eb
12 changed files with 128 additions and 114 deletions

View File

@@ -8,12 +8,14 @@
package com.facebook.flipper.plugins.uidebugger package com.facebook.flipper.plugins.uidebugger
import android.app.Application import android.app.Application
import android.util.Log
import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.core.FlipperPlugin 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.ApplicationInspector
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.Context 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 import kotlinx.serialization.json.Json
val LogTag = "FlipperUIDebugger" val LogTag = "FlipperUIDebugger"
@@ -32,17 +34,22 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin {
this.connection = connection this.connection = connection
// temp solution, get from descriptor // temp solution, get from descriptor
val inspector = ApplicationInspector(context) 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( connection.send(
InitEvent.name, InitEvent.name,
Json.encodeToString( Json.encodeToString(
InitEvent.serializer(), InitEvent(System.identityHashCode(application).toString()))) InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef))))
try {
val nodes = inspector.traversal.traverse()
connection.send( connection.send(
NativeScanEvent.name, NativeScanEvent.name,
Json.encodeToString(NativeScanEvent.serializer(), NativeScanEvent(root))) Json.encodeToString(NativeScanEvent.serializer(), NativeScanEvent(nodes)))
} catch (e: java.lang.Exception) {
Log.e(LogTag, e.message.toString(), e)
}
} }
@Throws(Exception::class) @Throws(Exception::class)

View File

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

View File

@@ -9,16 +9,11 @@ package com.facebook.flipper.plugins.uidebugger.core
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.common.Node
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
class ApplicationInspector(val context: Context) { class ApplicationInspector(val context: Context) {
val descriptorRegister = DescriptorRegister.withDefaults() val descriptorRegister = DescriptorRegister.withDefaults()
val traversal = LayoutTraversal(descriptorRegister) val traversal = LayoutTraversal(descriptorRegister, context.applicationRef)
fun inspect(): Node? {
return traversal.inspect(context.application)
}
fun attachListeners(view: View) { fun attachListeners(view: View) {
// An OnGlobalLayoutListener watches the entire hierarchy for layout changes // 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 onRootViewRemoved(view: View) {}
override fun onRootViewsChanged(views: java.util.List<View>) {} override fun onRootViewsChanged(views: java.util.List<View>) {}
}) })

View File

@@ -7,4 +7,4 @@
package com.facebook.flipper.plugins.uidebugger.core package com.facebook.flipper.plugins.uidebugger.core
class Context(val application: ApplicationRef) {} class Context(val applicationRef: ApplicationRef) {}

View File

@@ -7,63 +7,57 @@
package com.facebook.flipper.plugins.uidebugger.core 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.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.Node
import com.facebook.flipper.plugins.uidebugger.descriptors.Descriptor import com.facebook.flipper.plugins.uidebugger.descriptors.Descriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.model.Node
class LayoutTraversal(private val descriptorRegister: DescriptorRegister) { class LayoutTraversal(
class IntermediateNode(val node: Node) { private val descriptorRegister: DescriptorRegister,
var children: List<Any>? = null val root: ApplicationRef
} ) {
internal inline fun Descriptor<*>.asAny(): Descriptor<Any> = this as Descriptor<Any> internal inline fun Descriptor<*>.asAny(): Descriptor<Any> = this as Descriptor<Any>
private fun describe(obj: Any): IntermediateNode { /** Traverses the native android hierarchy */
var intermediate = IntermediateNode(Node()) fun traverse(): List<Node> {
val descriptor = descriptorRegister.descriptorForClass(obj::class.java) val result = mutableListOf<Node>()
descriptor?.let { descriptor -> val stack = mutableListOf<Any>()
val anyDescriptor = descriptor.asAny() stack.add(this.root)
intermediate.node.id = anyDescriptor.getId(obj) while (stack.isNotEmpty()) {
intermediate.node.name = anyDescriptor.getName(obj)
val attributes = mutableMapOf<String, InspectableObject>() val node = stack.removeLast()
anyDescriptor.getData(obj, attributes)
intermediate.node.attributes = attributes try {
val descriptor = descriptorRegister.descriptorForClassUnsafe(node::class.java).asAny()
val children = mutableListOf<Any>() val children = mutableListOf<Any>()
anyDescriptor.getChildren(obj, children) descriptor.getChildren(node, children)
intermediate.children = children
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)
} }
return intermediate val attributes = mutableMapOf<String, InspectableObject>()
} descriptor.getData(node, attributes)
private fun traverse(entry: Any): Node? { result.add(Node(descriptor.getId(node), descriptor.getName(node), attributes, childrenIds))
val root = describe(entry) } catch (exception: Exception) {
root?.let { intermediate -> Log.e(LogTag, "Error while processing node ${node.javaClass.name} ${node} ", exception)
val queue = mutableListOf<IntermediateNode>()
queue.add(intermediate)
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)
}
intermediateNode.node.children = children
} }
} }
return root?.node return result
}
fun inspect(applicationRef: ApplicationRef): Node? {
return traverse(applicationRef)
} }
} }

View File

@@ -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.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
class ApplicationDescriptor : AbstractChainedDescriptor<ApplicationRef>() { class ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
val rootResolver = RootViewResolver() val rootResolver = RootViewResolver()
override fun onInit() {} override fun onInit() {}

View File

@@ -13,6 +13,7 @@ import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import com.facebook.flipper.plugins.uidebugger.common.UIDebuggerException
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
class DescriptorRegister { class DescriptorRegister {
@@ -23,7 +24,7 @@ class DescriptorRegister {
fun withDefaults(): DescriptorRegister { fun withDefaults(): DescriptorRegister {
val mapping = DescriptorRegister() val mapping = DescriptorRegister()
mapping.register(Any::class.java, ObjectDescriptor()) 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(Activity::class.java, ActivityDescriptor())
mapping.register(Window::class.java, WindowDescriptor()) mapping.register(Window::class.java, WindowDescriptor())
mapping.register(ViewGroup::class.java, ViewGroupDescriptor()) mapping.register(ViewGroup::class.java, ViewGroupDescriptor())
@@ -57,11 +58,16 @@ class DescriptorRegister {
register[clazz] = descriptor register[clazz] = descriptor
} }
fun descriptorForClass(clazz: Class<*>): Descriptor<*>? { fun <T> descriptorForClass(clazz: Class<T>): Descriptor<T>? {
var clazz = clazz var clazz: Class<*> = clazz
while (!register.containsKey(clazz)) { while (!register.containsKey(clazz)) {
clazz = clazz.superclass 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}")
} }
} }

View File

@@ -5,21 +5,18 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package com.facebook.flipper.plugins.uidebugger package com.facebook.flipper.plugins.uidebugger.model
import com.facebook.flipper.plugins.uidebugger.common.Node
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class InitEvent(val rootId: String) { data class InitEvent(val rootId: String) {
companion object { companion object {
val name = "init" const val name = "init"
} }
} }
// TODO flatten the tree into normalised list
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class NativeScanEvent(val root: Node) { data class NativeScanEvent(val nodes: List<Node>) {
companion object { companion object {
val name = "nativeScan" const val name = "nativeScan"
} }
} }

View File

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

View File

@@ -14,43 +14,57 @@ import {Tree} from 'antd';
import type {DataNode} from 'antd/es/tree'; import type {DataNode} from 'antd/es/tree';
import {DownOutlined} from '@ant-design/icons'; import {DownOutlined} from '@ant-design/icons';
function treeToAntTree(uiNode: UINode): DataNode { // 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 treeToMapRec(node: UINode): void {
// result.set(node.id, node);
// for (const child of node.children) {
// treeToMapRec(child);
// }
// }
//
// treeToMapRec(uiNode);
//
// return result;
// }
function nodesToAntTree(root: Id, nodes: Map<Id, UINode>): DataNode {
function uiNodeToAntNode(id: Id): DataNode {
const node = nodes.get(id);
return { return {
key: uiNode.id, key: id,
title: uiNode.name, title: node?.name,
children: uiNode.children ? uiNode.children.map(treeToAntTree) : [], children: node?.children.map((id) => uiNodeToAntNode(id)),
}; };
}
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 uiNodeToAntNode(root);
return result;
} }
export function Component() { export function Component() {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const rootId = useValue(instance.rootId); const rootId = useValue(instance.rootId);
const tree = useValue(instance.tree); const nodes = useValue(instance.nodes);
if (tree) { if (rootId) {
const nodeMap = treeToMap(tree); const antTree = nodesToAntTree(rootId, nodes);
const antTree = treeToAntTree(tree); console.log(antTree);
console.log(rootId);
return ( return (
<Tree <Tree
showIcon showIcon
showLine showLine
onSelect={(selected) => { onSelect={(selected) => {
console.log(nodeMap.get(selected[0] as string)); console.log(nodes.get(selected[0] as string));
}} }}
defaultExpandAll defaultExpandAll
switcherIcon={<DownOutlined />} switcherIcon={<DownOutlined />}
@@ -59,5 +73,5 @@ export function Component() {
); );
} }
return <div>{rootId}</div>; return <div>Nothing yet</div>;
} }

View File

@@ -43,26 +43,26 @@ export type UINode = {
id: Id; id: Id;
name: string; name: string;
attributes: Record<string, Inspectable>; attributes: Record<string, Inspectable>;
children: UINode[]; children: Id[];
}; };
type Events = { type Events = {
init: {rootId: string}; init: {rootId: string};
nativeScan: {nodes: UINode[]};
nativeScan: {root: UINode};
}; };
export function plugin(client: PluginClient<Events>) { 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('init', (root) => rootId.set(root.rootId));
client.onMessage('nativeScan', ({root}) => { client.onMessage('nativeScan', ({nodes}) => {
tree.set(root as UINode); 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'; export {Component} from './components/main';