Fixing issue where certain activities not tracked

Summary:
The root view resolver will always find all root views but there was a bug in the listrootviews method. The code was very complex and most of the code seemed to be unneeded. Its now working. The listrootviews method now just returns teh contents of the observable array.

The reason we needed this is that Certain activities dont seem to tracked by the listener we add to `registerActivityLifecycleCallbacks` Its as if there is a floating decor with no activity. One example in FB4a is clicking on a notification in the notifications panel

Reviewed By: lblasa

Differential Revision: D41522791

fbshipit-source-id: b49b0104ddf758f097e1fd3f9ac6588de2d3646e
This commit is contained in:
Luke De Feo
2022-12-05 07:07:44 -08:00
committed by Facebook GitHub Bot
parent 29e0794ff4
commit 00334acb2b
5 changed files with 77 additions and 145 deletions

View File

@@ -9,31 +9,20 @@ package com.facebook.flipper.plugins.uidebugger.core
import android.app.Activity
import android.app.Application
import android.view.View
class ApplicationRef(val application: Application) {
init {
ActivityTracker.start(application)
}
// the root view resolver will contain all root views 100% It is needed for 2 cases:
// 1. In some cases an activity will not be picked up by the activity tracker,
// the root view resolver will at least find the decor view
// 2. Dialog fragments
val rootsResolver: RootViewResolver = RootViewResolver()
val activitiesStack: List<Activity>
get() {
return ActivityTracker.activitiesStack
}
val rootViews: List<View>
get() {
val activeRootViews = rootsResolver.listActiveRootViews()
activeRootViews?.let { roots ->
val viewRoots: MutableList<View> = ArrayList(roots.size)
for (root in roots) {
viewRoots.add(root.view)
}
return viewRoots
}
return emptyList()
}
}

View File

@@ -10,7 +10,6 @@ package com.facebook.flipper.plugins.uidebugger.core
import android.annotation.SuppressLint
import android.os.Build
import android.view.View
import android.view.WindowManager
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier
@@ -31,17 +30,80 @@ import java.lang.reflect.Modifier
class RootViewResolver {
private var initialized = false
private var windowManagerObj: Any? = null
private var viewsField: Field? = null
private var paramsField: Field? = null
private val observableViews: ObservableViewArrayList = ObservableViewArrayList()
class RootView(val view: View, val param: WindowManager.LayoutParams?)
interface Listener {
fun onRootViewAdded(rootView: View)
fun onRootViewRemoved(rootView: View)
fun onRootViewsChanged(rootViews: List<View>)
}
class ObservableArrayList : ArrayList<View>() {
fun attachListener(listener: Listener?) {
if (Build.VERSION.SDK_INT < 19 || listener == null) {
// We don't have a use for this on older APIs. If you do then modify accordingly :)
return
}
if (!initialized) {
initialize()
}
observableViews?.setListener(listener)
}
/**
* Lists the active root views in an application at this moment.
*
* @return a list of all the active root views in the application.
* @throws IllegalStateException if invoked from a thread besides the main thread.
*/
fun rootViews(): List<View> {
if (!initialized) {
initialize()
}
return observableViews.toList()
}
private fun initialize() {
initialized = true
val accessClass =
if (Build.VERSION.SDK_INT > 16) WINDOW_MANAGER_GLOBAL_CLAZZ else WINDOW_MANAGER_IMPL_CLAZZ
val instanceMethod = if (Build.VERSION.SDK_INT > 16) GET_GLOBAL_INSTANCE else GET_DEFAULT_IMPL
try {
val clazz = Class.forName(accessClass)
val getMethod = clazz.getMethod(instanceMethod)
windowManagerObj = getMethod.invoke(null)
val viewsField: Field = clazz.getDeclaredField(VIEWS_FIELD)
viewsField.let { vf ->
vf.isAccessible = true
// Forgive me father for I have sinned...
@SuppressLint("DiscouragedPrivateApi")
val modifiers = Field::class.java.getDeclaredField("accessFlags")
modifiers.isAccessible = true
modifiers.setInt(vf, vf.modifiers and Modifier.FINAL.inv())
@Suppress("unchecked_cast")
val currentWindowManagerViews = vf[windowManagerObj] as List<View>
observableViews.addAll(currentWindowManagerViews)
vf[windowManagerObj] = observableViews
}
} catch (ite: InvocationTargetException) {} catch (cnfe: ClassNotFoundException) {} catch (
nsfe: NoSuchFieldException) {} catch (nsme: NoSuchMethodException) {} catch (
re: RuntimeException) {} catch (iae: IllegalAccessException) {}
try {} catch (e: Throwable) {}
}
companion object {
private const val WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl"
private const val WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal"
private const val VIEWS_FIELD = "mViews"
private const val GET_DEFAULT_IMPL = "getDefault"
private const val GET_GLOBAL_INSTANCE = "getInstance"
}
class ObservableViewArrayList : ArrayList<View>() {
private var listener: Listener? = null
fun setListener(listener: Listener?) {
this.listener = listener
@@ -75,124 +137,4 @@ class RootViewResolver {
return view
}
}
fun attachListener(listener: Listener?) {
if (Build.VERSION.SDK_INT < 19 || listener == null) {
// We don't have a use for this on older APIs. If you do then modify accordingly :)
return
}
if (!initialized) {
initialize()
}
try {
viewsField?.let { vf ->
// Forgive me father for I have sinned...
@SuppressLint("DiscouragedPrivateApi")
val modifiers = Field::class.java.getDeclaredField("accessFlags")
modifiers.isAccessible = true
modifiers.setInt(vf, vf.modifiers and Modifier.FINAL.inv())
@Suppress("unchecked_cast") val views = vf[windowManagerObj] as List<View>
val observableViews = ObservableArrayList()
observableViews.setListener(listener)
observableViews.addAll(views)
vf[windowManagerObj] = observableViews
}
} catch (e: Throwable) {}
}
/**
* Lists the active root views in an application at this moment.
*
* @return a list of all the active root views in the application.
* @throws IllegalStateException if invoked from a thread besides the main thread.
*/
fun listActiveRootViews(): List<RootView>? {
if (!initialized) {
initialize()
}
if (null == windowManagerObj) {
return null
}
if (null == viewsField) {
return null
}
if (null == paramsField) {
return null
}
var maybeViews: List<View>? = null
var maybeParams: List<WindowManager.LayoutParams>? = null
try {
viewsField?.let { field ->
maybeViews =
if (Build.VERSION.SDK_INT < 19) {
@Suppress("unchecked_cast") val arr = field[windowManagerObj] as Array<View>
arr.toList()
} else {
@Suppress("unchecked_cast")
field[windowManagerObj] as List<View>
}
}
paramsField?.let { field ->
maybeParams =
if (Build.VERSION.SDK_INT < 19) {
@Suppress("unchecked_cast")
val arr = field[windowManagerObj] as Array<WindowManager.LayoutParams>
arr.toList()
} else {
@Suppress("unchecked_cast")
field[windowManagerObj] as List<WindowManager.LayoutParams>
}
}
} catch (re: RuntimeException) {
return null
} catch (iae: IllegalAccessException) {
return null
}
val roots = mutableListOf<RootView>()
maybeViews?.let { views ->
maybeParams?.let { params ->
if (views.size == params.size) {
for (i in views.indices) {
val view = views[i]
val param = params[i]
roots.add(RootView(view, param))
}
}
}
}
return roots
}
private fun initialize() {
initialized = true
val accessClass =
if (Build.VERSION.SDK_INT > 16) WINDOW_MANAGER_GLOBAL_CLAZZ else WINDOW_MANAGER_IMPL_CLAZZ
val instanceMethod = if (Build.VERSION.SDK_INT > 16) GET_GLOBAL_INSTANCE else GET_DEFAULT_IMPL
try {
val clazz = Class.forName(accessClass)
val getMethod = clazz.getMethod(instanceMethod)
windowManagerObj = getMethod.invoke(null)
viewsField = clazz.getDeclaredField(VIEWS_FIELD)
viewsField?.let { vf -> vf.isAccessible = true }
paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD)
paramsField?.let { pf -> pf.isAccessible = true }
} catch (ite: InvocationTargetException) {} catch (cnfe: ClassNotFoundException) {} catch (
nsfe: NoSuchFieldException) {} catch (nsme: NoSuchMethodException) {} catch (
re: RuntimeException) {} catch (iae: IllegalAccessException) {}
}
companion object {
private const val WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl"
private const val WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal"
private const val VIEWS_FIELD = "mViews"
private const val WINDOW_PARAMS_FIELD = "mParams"
private const val GET_DEFAULT_IMPL = "getDefault"
private const val GET_GLOBAL_INSTANCE = "getInstance"
}
}

View File

@@ -16,7 +16,8 @@ import com.facebook.flipper.plugins.uidebugger.util.DisplayMetrics
object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
override fun onGetActiveChild(node: ApplicationRef): Any? {
return if (node.activitiesStack.isNotEmpty()) node.activitiesStack.last() else null
val children = onGetChildren(node)
return if (children.isNotEmpty()) children.last() else null
}
override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds()
@@ -31,7 +32,7 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
override fun onGetChildren(node: ApplicationRef): List<Any> {
val children = mutableListOf<Any>()
val activeRoots = node.rootViews
val activeRoots = node.rootsResolver.rootViews()
val added = mutableSetOf<View>()
for (activity: Activity in node.activitiesStack) {

View File

@@ -40,9 +40,9 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRe
}
context.applicationRef.rootsResolver.attachListener(rootViewListener)
// On subscribe, trigger a traversal on whatever roots we have
rootViewListener.onRootViewsChanged(applicationRef.rootViews)
rootViewListener.onRootViewsChanged(applicationRef.rootsResolver.rootViews())
Log.i(LogTag, "${context.applicationRef.rootViews.size} root views")
Log.i(LogTag, "${context.applicationRef.rootsResolver.rootViews().size} root views")
Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities")
}

View File

@@ -174,7 +174,7 @@ function Visualization2DNode({
//if there is an active child don't draw the other children
//this means we don't draw overlapping activities / tabs etc
if (node.activeChildIdx) {
if (node.activeChildIdx && node.activeChildIdx < node.children.length) {
nestedChildren = [node.children[node.activeChildIdx]];
} else {
nestedChildren = node.children;