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
|
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)
|
||||||
|
|||||||
@@ -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.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>) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {}
|
||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 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> {
|
return uiNodeToAntNode(root);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user