Restore root view tracking

Summary: We use the root view resolver to detect when roots have been added. We currently have some race conditions where the activity stack managed by application ref doesnt include the first activity

Reviewed By: lblasa

Differential Revision: D39466929

fbshipit-source-id: fff4f830dea337d96dd9a9956a20a080fff2e965
This commit is contained in:
Luke De Feo
2022-09-13 11:05:42 -07:00
committed by Facebook GitHub Bot
parent 4341cbdf3d
commit f06f63306e
5 changed files with 57 additions and 123 deletions

View File

@@ -1,53 +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.core
import android.view.View
import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
class ApplicationInspector(val context: Context) {
val descriptorRegister = DescriptorRegister.withDefaults()
val traversal = LayoutTraversal(descriptorRegister, context.applicationRef)
fun attachListeners(view: View) {
// An OnGlobalLayoutListener watches the entire hierarchy for layout changes
// (so registering one of these on any View in a hierarchy will cause it to be triggered
// when any View in that hierarchy is laid out or changes visibility).
view
.getViewTreeObserver()
.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {}
})
view
.getViewTreeObserver()
.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return true
}
})
}
fun observe() {
val rootResolver = RootViewResolver()
rootResolver.attachListener(
object : RootViewResolver.Listener {
override fun onRootViewAdded(view: View) {
attachListeners(view)
}
override fun onRootViewRemoved(view: View) {}
override fun onRootViewsChanged(views: java.util.List<View>) {}
})
val activeRoots = rootResolver.listActiveRootViews()
activeRoots?.let { roots -> for (root: RootViewResolver.RootView in roots) {} }
}
}

View File

@@ -20,7 +20,7 @@ class ApplicationRef(val application: Application) : Application.ActivityLifecyc
fun onActivityDestroyed(activity: Activity, stack: List<Activity>) fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
} }
private val rootsResolver: RootViewResolver val rootsResolver: RootViewResolver
private val activities: MutableList<WeakReference<Activity>> private val activities: MutableList<WeakReference<Activity>>
private var activityStackChangedlistener: ActivityStackChangedListener? = null private var activityStackChangedlistener: ActivityStackChangedListener? = null

View File

@@ -14,7 +14,6 @@ import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.util.ArrayList import java.util.ArrayList
import java.util.List
/** /**
* Provides access to all root views in an application. * Provides access to all root views in an application.
@@ -39,28 +38,28 @@ class RootViewResolver {
interface Listener { interface Listener {
fun onRootViewAdded(rootView: View) fun onRootViewAdded(rootView: View)
fun onRootViewRemoved(rootView: View) fun onRootViewRemoved(rootView: View)
fun onRootViewsChanged(rootView: List<View>) fun onRootViewsChanged(rootViews: List<View>)
} }
class ObservableArrayList() : ArrayList<View>() { class ObservableArrayList : ArrayList<View>() {
private var listener: Listener? = null private var listener: Listener? = null
fun setListener(listener: Listener?) { fun setListener(listener: Listener?) {
this.listener = listener this.listener = listener
} }
override fun add(value: View): Boolean { override fun add(element: View): Boolean {
val ret = super.add(value) val ret = super.add(element)
listener?.let { l -> listener?.let { l ->
l.onRootViewAdded(value) l.onRootViewAdded(element)
l.onRootViewsChanged(this as List<View>) l.onRootViewsChanged(this as List<View>)
} }
return ret return ret
} }
override fun remove(value: View): Boolean { override fun remove(element: View): Boolean {
val ret = super.remove(value) val ret = super.remove(element)
listener?.let { l -> listener?.let { l ->
l.onRootViewRemoved(value) l.onRootViewRemoved(element)
l.onRootViewsChanged(this as List<View>) l.onRootViewsChanged(this as List<View>)
} }
@@ -128,7 +127,7 @@ class RootViewResolver {
viewsField?.let { field -> viewsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) { if (Build.VERSION.SDK_INT < 19) {
val arr = field[windowManagerObj] as Array<View> val arr = field[windowManagerObj] as Array<View>
views = arr.toList() as List<View> views = arr.toList()
} else { } else {
views = field[windowManagerObj] as List<View> views = field[windowManagerObj] as List<View>
} }
@@ -148,16 +147,20 @@ class RootViewResolver {
return null return null
} }
val roots: ArrayList<RootView> = ArrayList() val roots = mutableListOf<RootView>()
views?.let { views -> views?.let { views ->
params?.let { params -> params?.let { params ->
for (i in views.indices) { for (i in views.indices) {
roots.add(RootView(views[i], params[i])) val view = views[i]
// TODO FIX, len(param) is not always the same as len(views) For now just use first
val param = params[0]
// params
roots.add(RootView(view, param))
} }
} }
} }
return roots as List<RootView> return roots
} }
private fun initialize() { private fun initialize() {

View File

@@ -8,10 +8,14 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.app.Activity import android.app.Activity
import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
object ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() { object ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
val rootsLocal = RootViewResolver()
override fun onGetActiveChild(node: ApplicationRef): Any? { override fun onGetActiveChild(node: ApplicationRef): Any? {
return if (node.activitiesStack.isNotEmpty()) node.activitiesStack.last() else null return if (node.activitiesStack.isNotEmpty()) node.activitiesStack.last() else null
} }
@@ -28,8 +32,30 @@ object ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
} }
override fun onGetChildren(node: ApplicationRef, children: MutableList<Any>) { override fun onGetChildren(node: ApplicationRef, children: MutableList<Any>) {
for (activity: Activity in node.activitiesStack) { val activeRoots = node.rootViews
children.add(activity)
Log.i(LogTag, rootsLocal.toString())
activeRoots.let { roots ->
for (root in roots) {
var added = false
/**
* This code serves 2 purposes: 1.it picks up root views not tied to an activity (dialogs)
* 2. We can get initialized late and miss the first activity, it does seem that the root
* view resolver is able to (usually ) get the root view regardless, with this we insert the
* root decor view without the activity. Ideally we wouldn't rely on this behaviour and find
* a better way to track activities
*/
for (activity: Activity in node.activitiesStack) {
if (activity.window.decorView == root) {
children.add(activity)
added = true
break
}
}
if (!added) {
children.add(root)
}
}
} }
} }
} }

View File

@@ -7,16 +7,13 @@
package com.facebook.flipper.plugins.uidebugger.observers package com.facebook.flipper.plugins.uidebugger.observers
import android.app.Activity
import android.content.ContextWrapper
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.SubtreeUpdate
import com.facebook.flipper.plugins.uidebugger.TreeObserver import com.facebook.flipper.plugins.uidebugger.TreeObserver
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.identityHashCode import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
/** /**
* responsible for observing the activity stack and managing the subscription to the top most * responsible for observing the activity stack and managing the subscription to the top most
@@ -27,66 +24,27 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRe
override val type = "Application" override val type = "Application"
override fun subscribe(node: Any) { override fun subscribe(node: Any) {
Log.i(LogTag, "subscribing to application / activity changes") Log.i(LogTag, "Subscribing activity / root view changes")
val applicationRef = node as ApplicationRef val applicationRef = node as ApplicationRef
val addRemoveListener = val rootViewListener =
object : ApplicationRef.ActivityStackChangedListener { object : RootViewResolver.Listener {
override fun onRootViewAdded(rootView: View) {}
override fun onActivityAdded(activity: Activity, stack: List<Activity>) { override fun onRootViewRemoved(rootView: View) {}
val start = System.currentTimeMillis()
val (nodes, skipped) = context.layoutTraversal.traverse(applicationRef)
val observer =
context.observerFactory.createObserver(activity.window.decorView, context)!!
observer.subscribe(activity.window.decorView)
children[activity.window.decorView.identityHashCode()] = observer
context.treeObserverManager.emit(
SubtreeUpdate("Application", nodes, start, System.currentTimeMillis()))
Log.i(
LogTag,
"Activity added,stack size ${stack.size} found ${nodes.size} skipped $skipped Listeners $children")
}
override fun onActivityStackChanged(stack: List<Activity>) {} override fun onRootViewsChanged(rootViews: List<View>) {
Log.i(LogTag, "Root views updated, num ${rootViews.size}")
override fun onActivityDestroyed(activity: Activity, stack: List<Activity>) { traverseAndSend(context, applicationRef)
val start = System.currentTimeMillis()
val (nodes, skipped) = context.layoutTraversal.traverse(applicationRef)
val observer = children[activity.window.decorView.identityHashCode()]
children.remove(activity.window.decorView.identityHashCode())
observer?.cleanUpRecursive()
context.treeObserverManager.emit(
SubtreeUpdate("Application", nodes, start, System.currentTimeMillis()))
Log.i(
LogTag,
"Activity removed,stack size ${stack.size} found ${nodes.size} skipped $skipped Listeners $children")
} }
} }
context.applicationRef.rootsResolver.attachListener(rootViewListener)
context.applicationRef.setActivityStackChangedListener(addRemoveListener) // trigger a traversal on whatever roots we have now
rootViewListener.onRootViewsChanged(applicationRef.rootViews)
Log.i(LogTag, "${context.applicationRef.rootViews.size} root views") Log.i(LogTag, "${context.applicationRef.rootViews.size} root views")
Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities") Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities")
val stack = context.applicationRef.activitiesStack
for (activity in stack) {
addRemoveListener.onActivityAdded(activity, stack)
}
}
private fun getActivity(view: View): Activity? {
var context: android.content.Context? = view.context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = context.baseContext
}
return null
} }
override fun unsubscribe() { override fun unsubscribe() {