From 5b89331ea291f346959696f6a8c9979f4ddb8875 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Thu, 2 Nov 2023 12:29:07 -0700 Subject: [PATCH] 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 --- .../uidebugger/core/DecorViewTracker.kt | 31 ++++++++++++----- .../descriptors/ApplicationRefDescriptor.kt | 33 ++++++++++--------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/DecorViewTracker.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/DecorViewTracker.kt index 24720ff89..83aef111f 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/DecorViewTracker.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/DecorViewTracker.kt @@ -11,15 +11,30 @@ import android.util.Log import android.view.View import android.view.ViewTreeObserver 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.util.StopWatch import com.facebook.flipper.plugins.uidebugger.util.Throttler 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 - * to it This predraw observer triggers a full traversal of the UI. There should only ever be one - * active predraw listener at once + * The UIDebugger does 3 things: + * 1. Observe changes + * 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) { @@ -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 currentDecorView?.viewTreeObserver?.removeOnPreDrawListener(preDrawListener) - // setup new listener on top most view - val topView = rootViews.lastOrNull() - val throttler = Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } } + // setup new listener on top most view, that will be the active child in traversal + val topView = rootViews.lastOrNull(ApplicationRefDescriptor::isUsefulRoot) if (topView != null) { + val throttler = + Throttler(500) { currentDecorView?.let { traverseSnapshotAndSend(it) } } + preDrawListener = ViewTreeObserver.OnPreDrawListener { 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 throttler.trigger() - } else { - Log.i(LogTag, "Stack is empty") } } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt index 5278561bb..b5a2cde29 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ApplicationRefDescriptor.kt @@ -18,15 +18,7 @@ object ApplicationRefDescriptor : ChainedDescriptor() { override fun onGetActiveChild(node: ApplicationRef): Any? { val children = onGetChildren(node) - if (children.isNotEmpty()) { - val last = children.last() - if (last.javaClass.simpleName.contains("OverlayHandlerView")) { - return children.getOrNull(children.size - 2) - } - return last - } - - return null + return children.lastOrNull(ApplicationRefDescriptor::isUsefulRoot) } override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds() @@ -48,19 +40,30 @@ object ApplicationRefDescriptor : ChainedDescriptor() { for (root in activeRoots) { // 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] if (activity != null) { children.add(activity) } else { - if (root is ViewGroup && root.childCount > 0) { - // 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) - } + children.add(root) } } 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 + } + } }