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>)
}
private val rootsResolver: RootViewResolver
val rootsResolver: RootViewResolver
private val activities: MutableList<WeakReference<Activity>>
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.Modifier
import java.util.ArrayList
import java.util.List
/**
* Provides access to all root views in an application.
@@ -39,28 +38,28 @@ class RootViewResolver {
interface Listener {
fun onRootViewAdded(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
fun setListener(listener: Listener?) {
this.listener = listener
}
override fun add(value: View): Boolean {
val ret = super.add(value)
override fun add(element: View): Boolean {
val ret = super.add(element)
listener?.let { l ->
l.onRootViewAdded(value)
l.onRootViewAdded(element)
l.onRootViewsChanged(this as List<View>)
}
return ret
}
override fun remove(value: View): Boolean {
val ret = super.remove(value)
override fun remove(element: View): Boolean {
val ret = super.remove(element)
listener?.let { l ->
l.onRootViewRemoved(value)
l.onRootViewRemoved(element)
l.onRootViewsChanged(this as List<View>)
}
@@ -128,7 +127,7 @@ class RootViewResolver {
viewsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) {
val arr = field[windowManagerObj] as Array<View>
views = arr.toList() as List<View>
views = arr.toList()
} else {
views = field[windowManagerObj] as List<View>
}
@@ -148,16 +147,20 @@ class RootViewResolver {
return null
}
val roots: ArrayList<RootView> = ArrayList()
val roots = mutableListOf<RootView>()
views?.let { views ->
params?.let { params ->
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() {

View File

@@ -8,10 +8,14 @@
package com.facebook.flipper.plugins.uidebugger.descriptors
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.RootViewResolver
object ApplicationRefDescriptor : AbstractChainedDescriptor<ApplicationRef>() {
val rootsLocal = RootViewResolver()
override fun onGetActiveChild(node: ApplicationRef): Any? {
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>) {
for (activity: Activity in node.activitiesStack) {
children.add(activity)
val activeRoots = node.rootViews
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
import android.app.Activity
import android.content.ContextWrapper
import android.util.Log
import android.view.View
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.core.ApplicationRef
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
@@ -27,66 +24,27 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRe
override val type = "Application"
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 addRemoveListener =
object : ApplicationRef.ActivityStackChangedListener {
val rootViewListener =
object : RootViewResolver.Listener {
override fun onRootViewAdded(rootView: View) {}
override fun onActivityAdded(activity: Activity, stack: List<Activity>) {
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 onRootViewRemoved(rootView: View) {}
override fun onActivityStackChanged(stack: List<Activity>) {}
override fun onActivityDestroyed(activity: Activity, stack: List<Activity>) {
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")
override fun onRootViewsChanged(rootViews: List<View>) {
Log.i(LogTag, "Root views updated, num ${rootViews.size}")
traverseAndSend(context, applicationRef)
}
}
context.applicationRef.setActivityStackChangedListener(addRemoveListener)
context.applicationRef.rootsResolver.attachListener(rootViewListener)
// 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.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() {