Fix case where traversal was out of sync with snapshot

Summary:
Its was possible for the view tree observer to be observing one root but this can a dead root with no view in it. As a result the snapshot will be empty and the observer will never fire.

The layout traversal Applicaiton ref descriptor had logic to handle these dead roots, this logic is now extracted and shared between the descriptor in the traversal and by the decor view tracker so they are in sync

Reviewed By: lblasa

Differential Revision: D50848155

fbshipit-source-id: ce6da13df40632cbb7a302a59382b4907131d9f5
This commit is contained in:
Luke De Feo
2023-11-02 12:29:07 -07:00
committed by Facebook GitHub Bot
parent d26612d840
commit 5b89331ea2
2 changed files with 41 additions and 23 deletions

View File

@@ -11,15 +11,30 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.descriptors.ApplicationRefDescriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.ViewDescriptor import com.facebook.flipper.plugins.uidebugger.descriptors.ViewDescriptor
import com.facebook.flipper.plugins.uidebugger.util.StopWatch import com.facebook.flipper.plugins.uidebugger.util.StopWatch
import com.facebook.flipper.plugins.uidebugger.util.Throttler import com.facebook.flipper.plugins.uidebugger.util.Throttler
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity import com.facebook.flipper.plugins.uidebugger.util.objectIdentity
/** /**
* The responsibility of this class is to find the top most decor view and add a pre draw observer * The UIDebugger does 3 things:
* to it This predraw observer triggers a full traversal of the UI. There should only ever be one * 1. Observe changes
* active predraw listener at once * 2. Traverse UI hierarchy, gathering tree
* 3. Generate snapshot
*
* All 3 of these stages need to work on the same view else there will be major inconsistencies
*
* The first responsibility of this class is to track changes to root views, find the top most decor
* view and add a pre draw observer to it. There should only ever be one active predraw listener at
* once.
*
* This pre-draw observer triggers a full traversal of the UI, the traversal of the hierarchy might
* skip some branches (active child) so its essential that both the active child decision and top
* root decision match.
*
* The observer also triggers a snapshot, again its essential the same root view as we do for
* traversal and observation
*/ */
class DecorViewTracker(private val context: UIDContext, private val snapshotter: Snapshotter) { class DecorViewTracker(private val context: UIDContext, private val snapshotter: Snapshotter) {
@@ -42,11 +57,13 @@ class DecorViewTracker(private val context: UIDContext, private val snapshotter:
// remove predraw listen from current view as its going away or will be covered // remove predraw listen from current view as its going away or will be covered
currentDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener) currentDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener)
// setup new listener on top most view // setup new listener on top most view, that will be the active child in traversal
val topView = rootViews.lastOrNull() val topView = rootViews.lastOrNull(ApplicationRefDescriptor::isUsefulRoot)
val throttler = Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } }
if (topView != null) { if (topView != null) {
val throttler =
Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } }
preDrawListener = preDrawListener =
ViewTreeObserver.OnPreDrawListener { ViewTreeObserver.OnPreDrawListener {
throttler.trigger() throttler.trigger()
@@ -60,8 +77,6 @@ class DecorViewTracker(private val context: UIDContext, private val snapshotter:
// schedule traversal immediately when we detect a new decor view // schedule traversal immediately when we detect a new decor view
throttler.trigger() throttler.trigger()
} else {
Log.i(LogTag, "Stack is empty")
} }
} }
} }

View File

@@ -18,15 +18,7 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
override fun onGetActiveChild(node: ApplicationRef): Any? { override fun onGetActiveChild(node: ApplicationRef): Any? {
val children = onGetChildren(node) val children = onGetChildren(node)
if (children.isNotEmpty()) { return children.lastOrNull(ApplicationRefDescriptor::isUsefulRoot)
val last = children.last()
if (last.javaClass.simpleName.contains("OverlayHandlerView")) {
return children.getOrNull(children.size - 2)
}
return last
}
return null
} }
override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds() override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds()
@@ -48,19 +40,30 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
for (root in activeRoots) { for (root in activeRoots) {
// if there is an activity for this root view use that, // if there is an activity for this root view use that,
// if not just return the mystery floating decor view // if not just return the root view that was added directly to the window manager
val activity = decorViewToActivity[root] val activity = decorViewToActivity[root]
if (activity != null) { if (activity != null) {
children.add(activity) children.add(activity)
} else { } else {
if (root is ViewGroup && root.childCount > 0) { children.add(root)
// sometimes there is a root view on top that has no children and we dont want to add
// these as they will become active
children.add(root)
}
} }
} }
return children return children
} }
fun isUsefulRoot(obj: Any): Boolean {
if (obj is Activity) {
return true
}
val isFoldableOverlayInfraView = javaClass.simpleName.contains("OverlayHandlerView")
return if (isFoldableOverlayInfraView) {
false
} else if (obj is ViewGroup) {
// sometimes there is a root view on top that has no children that isn't useful to inspect
obj.childCount > 0
} else {
false
}
}
} }