Addresses fragments tracking and different bugs/warnings

Summary:
^

After this change lands, it is safe to remove most of the Stetho fragment support types.

Reviewed By: LukeDefeo

Differential Revision: D39460121

fbshipit-source-id: 0e7d4ce71e828ee7bc9c6e945b8fe27dbd6f08f8
This commit is contained in:
Lorenzo Blasa
2022-09-20 05:15:50 -07:00
committed by Facebook GitHub Bot
parent e8392bdceb
commit 86364cbd40
21 changed files with 565 additions and 194 deletions

View File

@@ -9,8 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.litho
import android.util.Log import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserver
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder
import com.facebook.litho.LithoView import com.facebook.litho.LithoView

View File

@@ -0,0 +1,133 @@
/*
* 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.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.os.Build
import android.os.Bundle
import java.lang.ref.WeakReference
import java.lang.reflect.Field
import java.lang.reflect.Method
object ActivityTracker : Application.ActivityLifecycleCallbacks {
interface ActivityStackChangedListener {
fun onActivityAdded(activity: Activity, stack: List<Activity>)
fun onActivityStackChanged(stack: List<Activity>)
fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
}
private val activities: MutableList<WeakReference<Activity>> = mutableListOf()
private val trackedActivities: MutableSet<Int> = mutableSetOf()
private var activityStackChangedListener: ActivityStackChangedListener? = null
fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
activityStackChangedListener = listener
}
fun start(application: Application) {
initialiseActivities()
application.registerActivityLifecycleCallbacks(this)
}
private fun trackActivity(activity: Activity) {
if (trackedActivities.contains(System.identityHashCode(activity))) {
return
}
trackedActivities.add(System.identityHashCode(activity))
activities.add(WeakReference<Activity>(activity))
FragmentTracker.trackFragmentsOfActivity(activity)
activityStackChangedListener?.onActivityAdded(activity, this.activitiesStack)
activityStackChangedListener?.onActivityStackChanged(this.activitiesStack)
}
private fun untrackActivity(activity: Activity) {
trackedActivities.remove(System.identityHashCode(activity))
val activityIterator: MutableIterator<WeakReference<Activity>> = activities.iterator()
while (activityIterator.hasNext()) {
if (activityIterator.next().get() === activity) {
activityIterator.remove()
}
}
FragmentTracker.untrackFragmentsOfActivity(activity)
activityStackChangedListener?.onActivityDestroyed(activity, this.activitiesStack)
activityStackChangedListener?.onActivityStackChanged(this.activitiesStack)
}
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
trackActivity(activity)
}
override fun onActivityStarted(activity: Activity) {
trackActivity(activity)
}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
untrackActivity(activity)
}
val activitiesStack: List<Activity>
get() {
val stack: MutableList<Activity> = ArrayList(activities.size)
val activityIterator: MutableIterator<WeakReference<Activity>> = activities.iterator()
while (activityIterator.hasNext()) {
val activity: Activity? = activityIterator.next().get()
if (activity == null) {
activityIterator.remove()
} else {
stack.add(activity)
}
}
return stack
}
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
fun initialiseActivities() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
try {
val activityThreadClass: Class<*> = Class.forName("android.app.ActivityThread")
val currentActivityThreadMethod: Method =
activityThreadClass.getMethod("currentActivityThread")
val currentActivityThread: Any? = currentActivityThreadMethod.invoke(null)
currentActivityThread?.let { activityThread ->
val mActivitiesField: Field = activityThreadClass.getDeclaredField("mActivities")
mActivitiesField.isAccessible = true
val mActivities = mActivitiesField.get(activityThread) as android.util.ArrayMap<*, *>
for (record in mActivities.values) {
val recordClass: Class<*> = record.javaClass
val activityField: Field = recordClass.getDeclaredField("activity")
activityField.isAccessible = true
val activity = activityField.get(record)
if (activity != null && activity is Activity) {
trackActivity(activity)
}
}
}
} catch (e: Exception) {}
}
}

View File

@@ -9,68 +9,25 @@ package com.facebook.flipper.plugins.uidebugger.core
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.os.Bundle
import android.view.View import android.view.View
import java.lang.ref.WeakReference
class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks { class ApplicationRef(val application: Application) {
interface ActivityStackChangedListener { init {
fun onActivityAdded(activity: Activity, stack: List<Activity>) ActivityTracker.start(application)
fun onActivityStackChanged(stack: List<Activity>)
fun onActivityDestroyed(activity: Activity, stack: List<Activity>)
} }
val rootsResolver: RootViewResolver val rootsResolver: RootViewResolver = RootViewResolver()
private val activities: MutableList<WeakReference<Activity>>
private var activityStackChangedlistener: ActivityStackChangedListener? = null
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities.add(WeakReference<Activity>(activity))
activityStackChangedlistener?.onActivityAdded(activity, this.activitiesStack)
activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
val activityIterator: MutableIterator<WeakReference<Activity>> = activities.iterator()
while (activityIterator.hasNext()) {
if (activityIterator.next().get() === activity) {
activityIterator.remove()
}
}
activityStackChangedlistener?.onActivityDestroyed(activity, this.activitiesStack)
activityStackChangedlistener?.onActivityStackChanged(this.activitiesStack)
}
fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) {
activityStackChangedlistener = listener
}
val activitiesStack: List<Activity> val activitiesStack: List<Activity>
get() { get() {
val stack: MutableList<Activity> = ArrayList<Activity>(activities.size) return ActivityTracker.activitiesStack
val activityIterator: MutableIterator<WeakReference<Activity>> = activities.iterator()
while (activityIterator.hasNext()) {
val activity: Activity? = activityIterator.next().get()
if (activity == null) {
activityIterator.remove()
} else {
stack.add(activity)
}
}
return stack
} }
val rootViews: List<View> val rootViews: List<View>
get() { get() {
val roots = rootsResolver.listActiveRootViews() val activeRootViews = rootsResolver.listActiveRootViews()
roots?.let { roots -> activeRootViews?.let { roots ->
val viewRoots: MutableList<View> = ArrayList<View>(roots.size) val viewRoots: MutableList<View> = ArrayList(roots.size)
for (root in roots) { for (root in roots) {
viewRoots.add(root.view) viewRoots.add(root.view)
} }
@@ -79,10 +36,4 @@ class ApplicationRef(val application: Application) : Application.ActivityLifecyc
return emptyList() return emptyList()
} }
init {
rootsResolver = RootViewResolver()
application.registerActivityLifecycleCallbacks(this)
activities = ArrayList<WeakReference<Activity>>()
}
} }

View File

@@ -0,0 +1,271 @@
/*
* 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.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.View
import java.lang.ref.WeakReference
object FragmentTracker {
class FragmentRef {
val supportFragment: WeakReference<androidx.fragment.app.Fragment>?
val frameworkFragment: WeakReference<android.app.Fragment>?
val isSupportFragment: Boolean
constructor(supportFragment: androidx.fragment.app.Fragment) {
this.supportFragment = WeakReference(supportFragment)
this.frameworkFragment = null
this.isSupportFragment = true
}
constructor(frameworkFragment: android.app.Fragment) {
this.supportFragment = null
this.frameworkFragment = WeakReference(frameworkFragment)
this.isSupportFragment = false
}
override fun hashCode(): Int {
if (isSupportFragment) {
val fragment = supportFragment?.get()
fragment?.let { f ->
return System.identityHashCode(f)
}
} else {
val fragment = frameworkFragment?.get()
fragment?.let { f ->
return System.identityHashCode(f)
}
}
return -1
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FragmentRef
if (isSupportFragment != other.isSupportFragment) return false
if (isSupportFragment && (supportFragment != other.supportFragment)) return false
else if (frameworkFragment != other.frameworkFragment) return false
return true
}
val view: View?
get() {
if (isSupportFragment) {
val fragment = supportFragment?.get()
fragment?.let { f ->
return f.view
}
} else {
val fragment = frameworkFragment?.get()
fragment?.let { f ->
return f.view
}
}
return null
}
override fun toString(): String {
if (isSupportFragment) {
val fragment = supportFragment?.get()
fragment?.let { f ->
return "$f"
}
} else {
val fragment = frameworkFragment?.get()
fragment?.let { f ->
return "$f"
}
}
return "unknown"
}
}
private val activityFragments: MutableMap<Int, MutableSet<FragmentRef>> = mutableMapOf()
private val viewFragment: MutableMap<Int, FragmentRef> = mutableMapOf()
private val supportLibraryLifecycleTracker =
object : androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(
fm: androidx.fragment.app.FragmentManager,
f: androidx.fragment.app.Fragment,
context: android.content.Context
) {
super.onFragmentAttached(fm, f, context)
f.activity?.let { activity ->
val fragmentRef = FragmentRef(f)
activityFragments[System.identityHashCode(activity)]?.add(fragmentRef)
}
}
override fun onFragmentViewCreated(
fm: androidx.fragment.app.FragmentManager,
f: androidx.fragment.app.Fragment,
v: View,
savedInstanceState: Bundle?
) {
super.onFragmentViewCreated(fm, f, v, savedInstanceState)
val fragmentRef = FragmentRef(f)
viewFragment[System.identityHashCode(v)] = fragmentRef
}
override fun onFragmentViewDestroyed(
fm: androidx.fragment.app.FragmentManager,
f: androidx.fragment.app.Fragment
) {
super.onFragmentViewDestroyed(fm, f)
for (entry in viewFragment) {
if (entry.value.supportFragment == f) {
viewFragment.remove(entry.key)
break
}
}
}
override fun onFragmentDetached(
fm: androidx.fragment.app.FragmentManager,
f: androidx.fragment.app.Fragment
) {
super.onFragmentDetached(fm, f)
f.activity?.let { activity ->
val fragmentRef = FragmentRef(f)
activityFragments[System.identityHashCode(activity)]?.remove(fragmentRef)
}
f.view?.let { view -> viewFragment.remove(System.identityHashCode(view)) }
}
}
private var frameworkLifecycleTracker: Any? = null
fun trackFragmentsOfActivity(activity: Activity) {
activityFragments[System.identityHashCode(activity)] = mutableSetOf()
if (activity is androidx.fragment.app.FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
supportLibraryLifecycleTracker, true)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && frameworkLifecycleTracker != null) {
activity.fragmentManager.registerFragmentLifecycleCallbacks(
frameworkLifecycleTracker as android.app.FragmentManager.FragmentLifecycleCallbacks,
true)
}
}
}
fun untrackFragmentsOfActivity(activity: Activity) {
activityFragments[System.identityHashCode(activity)]?.let { fragments ->
fragments.forEach { f ->
f.view?.let { view -> viewFragment.remove(System.identityHashCode(view)) }
}
}
activityFragments.remove(System.identityHashCode(activity))
if (activity is androidx.fragment.app.FragmentActivity) {
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(
supportLibraryLifecycleTracker)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && frameworkLifecycleTracker != null) {
activity.fragmentManager.unregisterFragmentLifecycleCallbacks(
frameworkLifecycleTracker as android.app.FragmentManager.FragmentLifecycleCallbacks)
}
}
}
fun getFragment(view: View): Any? {
val key = System.identityHashCode(view)
val fragmentRef = viewFragment[key]
fragmentRef?.let { fragment ->
fragment.supportFragment?.get()?.let { f ->
return f
}
fragment.frameworkFragment?.get()?.let { f ->
return f
}
}
return null
}
fun getDialogFragments(activity: Activity): List<Any> {
val key = System.identityHashCode(activity)
val fragments = mutableListOf<Any>()
activityFragments[key]?.forEach { fragmentRef ->
fragmentRef.supportFragment?.get()?.let { fragment ->
if (androidx.fragment.app.DialogFragment::class.java.isInstance(fragment)) {
fragments.add(fragmentRef)
}
}
fragmentRef.frameworkFragment?.get()?.let { fragment ->
if (android.app.DialogFragment::class.java.isInstance(fragment)) {
fragments.add(fragmentRef)
}
}
}
return fragments
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
Class.forName("android.app.FragmentManager\$FragmentLifecycleCallbacks") != null) {
frameworkLifecycleTracker =
object : android.app.FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(
fm: android.app.FragmentManager?,
f: android.app.Fragment?,
context: android.content.Context?
) {
super.onFragmentAttached(fm, f, context)
f?.let { fragment ->
val fragmentRef = FragmentRef(fragment)
activityFragments[System.identityHashCode(fragment.activity)]?.add(fragmentRef)
}
}
override fun onFragmentViewCreated(
fm: android.app.FragmentManager?,
f: android.app.Fragment?,
v: View?,
savedInstanceState: Bundle?
) {
super.onFragmentViewCreated(fm, f, v, savedInstanceState)
if (f != null && v != null) {
val fragmentRef = FragmentRef(f)
viewFragment[System.identityHashCode(v)] = fragmentRef
}
}
override fun onFragmentDetached(
fm: android.app.FragmentManager?,
f: android.app.Fragment?
) {
super.onFragmentDetached(fm, f)
f?.let { fragment ->
val fragmentRef = FragmentRef(fragment)
activityFragments[System.identityHashCode(fragment.activity)]?.remove(fragmentRef)
fragment.view?.let { view -> viewFragment.remove(System.identityHashCode(view)) }
}
}
}
}
}
}

View File

@@ -46,7 +46,7 @@ class LayoutTraversal(
val childDescriptor = val childDescriptor =
descriptorRegister.descriptorForClassUnsafe(child::class.java).asAny() descriptorRegister.descriptorForClassUnsafe(child::class.java).asAny()
childrenIds.add(childDescriptor.getId(child)) childrenIds.add(childDescriptor.getId(child))
// if there is an active child then dont traverse it // if there is an active child then don't traverse it
if (activeChild == null) { if (activeChild == null) {
stack.add(child) stack.add(child)
} }

View File

@@ -11,7 +11,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
/** Layout Visitor traverses the entire view hierarchy from a given root. */ /** Layout Visitor traverses the entire view hierarchy from a given root. */
class LayoutVisitor(val visitor: Visitor) { class LayoutVisitor(private val visitor: Visitor) {
interface Visitor { interface Visitor {
fun visit(view: View) fun visit(view: View)
} }

View File

@@ -13,7 +13,6 @@ import android.view.WindowManager
import java.lang.reflect.Field 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
/** /**
* Provides access to all root views in an application. * Provides access to all root views in an application.
@@ -125,21 +124,23 @@ class RootViewResolver {
var params: List<WindowManager.LayoutParams>? = null var params: List<WindowManager.LayoutParams>? = null
try { try {
viewsField?.let { field -> viewsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) { views =
val arr = field[windowManagerObj] as Array<View> if (Build.VERSION.SDK_INT < 19) {
views = arr.toList() val arr = field[windowManagerObj] as Array<View>
} else { arr.toList()
views = field[windowManagerObj] as List<View> } else {
} field[windowManagerObj] as List<View>
}
} }
paramsField?.let { field -> paramsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) { params =
val arr = field[windowManagerObj] as Array<WindowManager.LayoutParams> if (Build.VERSION.SDK_INT < 19) {
params = arr.toList() as List<WindowManager.LayoutParams> val arr = field[windowManagerObj] as Array<WindowManager.LayoutParams>
} else { arr.toList() as List<WindowManager.LayoutParams>
params = field[windowManagerObj] as List<WindowManager.LayoutParams> } else {
} field[windowManagerObj] as List<WindowManager.LayoutParams>
}
} }
} catch (re: RuntimeException) { } catch (re: RuntimeException) {
return null return null
@@ -150,12 +151,12 @@ class RootViewResolver {
val roots = mutableListOf<RootView>() val roots = mutableListOf<RootView>()
views?.let { views -> views?.let { views ->
params?.let { params -> params?.let { params ->
for (i in views.indices) { if (views.size == params.size) {
val view = views[i] for (i in views.indices) {
// TODO FIX, len(param) is not always the same as len(views) For now just use first val view = views[i]
val param = params[i]
// params roots.add(RootView(view, param))
roots.add(RootView(view, null)) }
} }
} }
} }

View File

@@ -9,7 +9,7 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
import android.app.Activity import android.app.Activity
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.stetho.FragmentCompat import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
object ActivityDescriptor : ChainedDescriptor<Activity>() { object ActivityDescriptor : ChainedDescriptor<Activity>() {
@@ -24,30 +24,12 @@ object ActivityDescriptor : ChainedDescriptor<Activity>() {
override fun onGetChildren(node: Activity, children: MutableList<Any>) { override fun onGetChildren(node: Activity, children: MutableList<Any>) {
node.window?.let { window -> children.add(window) } node.window?.let { window -> children.add(window) }
var fragments = getDialogFragments(FragmentCompat.supportInstance, node) val fragments = FragmentTracker.getDialogFragments(node)
for (fragment in fragments) { fragments.forEach { fragment -> children.add(fragment) }
children.add(fragment)
}
fragments = getDialogFragments(FragmentCompat.frameworkInstance, node)
for (fragment in fragments) {
children.add(fragment)
}
} }
override fun onGetData( override fun onGetData(
node: Activity, node: Activity,
attributeSections: MutableMap<String, InspectableObject> attributeSections: MutableMap<String, InspectableObject>
) {} ) {}
private fun getDialogFragments(
compat: FragmentCompat<*, *, *, *>?,
activity: Activity
): List<Any> {
if (compat == null) {
return emptyList()
}
return compat.getDialogFragments(activity)
}
} }

View File

@@ -8,14 +8,11 @@
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 android.view.View
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 : ChainedDescriptor<ApplicationRef>() { object ApplicationRefDescriptor : ChainedDescriptor<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
} }
@@ -34,27 +31,17 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
override fun onGetChildren(node: ApplicationRef, children: MutableList<Any>) { override fun onGetChildren(node: ApplicationRef, children: MutableList<Any>) {
val activeRoots = node.rootViews val activeRoots = node.rootViews
Log.i(LogTag, rootsLocal.toString()) val added = mutableSetOf<View>()
activeRoots.let { roots -> for (activity: Activity in node.activitiesStack) {
for (root in roots) { children.add(activity)
var added = false added.add(activity.window.decorView)
/** }
* 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 // Picks up root views not tied to an activity (dialogs)
* view resolver is able to (usually ) get the root view regardless, with this we insert the for (root in activeRoots) {
* root decor view without the activity. Ideally we wouldn't rely on this behaviour and find if (!added.contains(root)) {
* a better way to track activities children.add(root)
*/ added.add(root)
for (activity: Activity in node.activitiesStack) {
if (activity.window.decorView == root) {
children.add(activity)
added = true
break
}
}
if (!added) {
children.add(root)
}
} }
} }
} }

View File

@@ -33,17 +33,22 @@ class DescriptorRegister {
mapping.register(TextView::class.java, TextViewDescriptor) mapping.register(TextView::class.java, TextViewDescriptor)
mapping.register(Button::class.java, ButtonDescriptor) mapping.register(Button::class.java, ButtonDescriptor)
mapping.register(ViewPager::class.java, ViewPagerDescriptor) mapping.register(ViewPager::class.java, ViewPagerDescriptor)
mapping.register(android.app.Fragment::class.java, FragmentFrameworkDescriptor)
mapping.register(androidx.fragment.app.Fragment::class.java, FragmentSupportDescriptor)
@Suppress("UNCHECKED_CAST")
for (clazz in mapping.register.keys) { for (clazz in mapping.register.keys) {
val descriptor: NodeDescriptor<*>? = mapping.register[clazz] val maybeDescriptor: NodeDescriptor<*>? = mapping.register[clazz]
descriptor?.let { descriptor -> maybeDescriptor?.let { descriptor ->
if (descriptor is ChainedDescriptor<*>) { if (descriptor is ChainedDescriptor<*>) {
val chainedDescriptor = descriptor as ChainedDescriptor<Any> val chainedDescriptor = descriptor as ChainedDescriptor<Any>
val superClass: Class<*> = clazz.getSuperclass() val superClass: Class<*> = clazz.superclass
val superDescriptor: NodeDescriptor<*>? = mapping.descriptorForClass(superClass) val maybeSuperDescriptor: NodeDescriptor<*>? = mapping.descriptorForClass(superClass)
// todo we should walk all the way up the superclass hierarchy?
if (superDescriptor is ChainedDescriptor<*>) { maybeSuperDescriptor?.let { superDescriptor ->
chainedDescriptor.setSuper(superDescriptor as ChainedDescriptor<Any>) if (superDescriptor is ChainedDescriptor<*>) {
chainedDescriptor.setSuper(superDescriptor as ChainedDescriptor<Any>)
}
} }
} }
} }
@@ -58,11 +63,17 @@ class DescriptorRegister {
} }
fun <T> descriptorForClass(clazz: Class<T>): NodeDescriptor<T>? { fun <T> descriptorForClass(clazz: Class<T>): NodeDescriptor<T>? {
var clazz: Class<*> = clazz var mutableClass: Class<*> = clazz
while (!register.containsKey(clazz)) { while (!register.containsKey(mutableClass)) {
clazz = clazz.superclass mutableClass = mutableClass.superclass
}
return if (register[clazz] != null) {
@Suppress("UNCHECKED_CAST")
register[clazz] as NodeDescriptor<T>
} else {
null
} }
return register[clazz] as NodeDescriptor<T>
} }
fun <T> descriptorForClassUnsafe(clazz: Class<T>): NodeDescriptor<T> { fun <T> descriptorForClassUnsafe(clazz: Class<T>): NodeDescriptor<T> {

View File

@@ -0,0 +1,30 @@
/*
* 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.descriptors
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
object FragmentFrameworkDescriptor : ChainedDescriptor<android.app.Fragment>() {
override fun onGetId(node: android.app.Fragment): String {
return System.identityHashCode(node).toString()
}
override fun onGetName(node: android.app.Fragment): String {
return node.javaClass.simpleName
}
override fun onGetChildren(node: android.app.Fragment, children: MutableList<Any>) {
node.view?.let { view -> children.add(view) }
}
override fun onGetData(
node: android.app.Fragment,
attributeSections: MutableMap<String, InspectableObject>
) {}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.descriptors
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
object FragmentSupportDescriptor : ChainedDescriptor<androidx.fragment.app.Fragment>() {
override fun onGetId(node: androidx.fragment.app.Fragment): String {
return System.identityHashCode(node).toString()
}
override fun onGetName(node: androidx.fragment.app.Fragment): String {
return node.javaClass.simpleName
}
override fun onGetChildren(node: androidx.fragment.app.Fragment, children: MutableList<Any>) {
node.view?.let { view -> children.add(view) }
}
override fun onGetData(
node: androidx.fragment.app.Fragment,
attributeSections: MutableMap<String, InspectableObject>
) {}
}

View File

@@ -7,7 +7,6 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.app.Fragment
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -16,7 +15,7 @@ import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.common.Inspectable import com.facebook.flipper.plugins.uidebugger.common.Inspectable
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue import com.facebook.flipper.plugins.uidebugger.common.InspectableValue
import com.facebook.flipper.plugins.uidebugger.stetho.FragmentCompat import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() { object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() {
@@ -32,8 +31,8 @@ object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() {
val count = node.childCount - 1 val count = node.childCount - 1
for (i in 0..count) { for (i in 0..count) {
val child: View = node.getChildAt(i) val child: View = node.getChildAt(i)
val fragment = getAttachedFragmentForView(child) val fragment = FragmentTracker.getFragment(child)
if (fragment != null && !FragmentCompat.isDialogFragment(fragment)) { if (fragment != null) {
children.add(fragment) children.add(fragment)
} else children.add(child) } else children.add(child)
} }
@@ -63,19 +62,4 @@ object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() {
"LAYOUT_MODE_CLIP_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS, "LAYOUT_MODE_CLIP_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS,
"LAYOUT_MODE_OPTICAL_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS, "LAYOUT_MODE_OPTICAL_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS,
)) {} )) {}
private fun getAttachedFragmentForView(v: View): Any? {
return try {
val fragment = FragmentCompat.findFragmentForView(v)
var added = false
if (fragment is Fragment) {
added = fragment.isAdded
} else if (fragment is androidx.fragment.app.Fragment) {
added = fragment.isAdded
}
if (added) fragment else null
} catch (e: RuntimeException) {
null
}
}
} }

View File

@@ -10,13 +10,12 @@ package com.facebook.flipper.plugins.uidebugger.observers
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.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.core.RootViewResolver 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
* content view (decor view) * content view (decor view)
*/ */
class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRef>() { class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRef>() {
@@ -48,6 +47,7 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver<ApplicationRe
} }
override fun unsubscribe() { override fun unsubscribe() {
context.applicationRef.setActivityStackChangedListener(null) // Not entirely sure this will ever happen or be needed.
// context.applicationRef.setActivityStackChangedListener(null)
} }
} }

View File

@@ -11,7 +11,6 @@ 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.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.core.Context
typealias DecorView = View typealias DecorView = View

View File

@@ -7,11 +7,9 @@
package com.facebook.flipper.plugins.uidebugger.observers package com.facebook.flipper.plugins.uidebugger.observers
import com.facebook.flipper.plugins.uidebugger.TreeObserver
import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.core.Context
interface TreeObserverBuilder<T> { interface TreeObserverBuilder<T> {
fun canBuildFor(node: Any): Boolean fun canBuildFor(node: Any): Boolean
fun build(context: Context): TreeObserver<T> fun build(context: Context): TreeObserver<T>
} }

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package com.facebook.flipper.plugins.uidebugger package com.facebook.flipper.plugins.uidebugger.observers
import android.util.Log import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.core.Context import com.facebook.flipper.plugins.uidebugger.core.Context
import com.facebook.flipper.plugins.uidebugger.observers.SubtreeUpdate
/* /*
Stateful class that manages some subtree in the UI Hierarchy. Stateful class that manages some subtree in the UI Hierarchy.
@@ -75,7 +75,7 @@ abstract class TreeObserver<T> {
} }
fun cleanUpRecursive() { fun cleanUpRecursive() {
Log.i(LogTag, "Cleaning up observer ${this}") Log.i(LogTag, "Cleaning up observer $this")
children.values.forEach { it.cleanUpRecursive() } children.values.forEach { it.cleanUpRecursive() }
unsubscribe() unsubscribe()
children.clear() children.clear()

View File

@@ -44,7 +44,7 @@ object AccessibilityUtil {
* @param node The [AccessibilityNodeInfoCompat] to evaluate * @param node The [AccessibilityNodeInfoCompat] to evaluate
* @return `true` if it meets the criterion for producing spoken feedback * @return `true` if it meets the criterion for producing spoken feedback
*/ */
fun isSpeakingNode(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { private fun isSpeakingNode(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
if (node == null || view == null) { if (node == null || view == null) {
return false return false
} }
@@ -69,7 +69,7 @@ object AccessibilityUtil {
* @param node The [AccessibilityNodeInfoCompat] to evaluate * @param node The [AccessibilityNodeInfoCompat] to evaluate
* @return `true` if it has any non-actionable speaking descendants within its subtree * @return `true` if it has any non-actionable speaking descendants within its subtree
*/ */
fun hasNonActionableSpeakingDescendants( private fun hasNonActionableSpeakingDescendants(
node: AccessibilityNodeInfoCompat?, node: AccessibilityNodeInfoCompat?,
view: View? view: View?
): Boolean { ): Boolean {
@@ -79,10 +79,7 @@ object AccessibilityUtil {
val viewGroup = view as ViewGroup val viewGroup = view as ViewGroup
val count = viewGroup.childCount - 1 val count = viewGroup.childCount - 1
for (i in 0..count) { for (i in 0..count) {
val childView = viewGroup.getChildAt(i) val childView = viewGroup.getChildAt(i) ?: continue
if (childView == null) {
continue
}
val childNode = AccessibilityNodeInfoCompat.obtain() val childNode = AccessibilityNodeInfoCompat.obtain()
try { try {
ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode) ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode)
@@ -107,7 +104,7 @@ object AccessibilityUtil {
* @param node The [AccessibilityNodeInfoCompat] to evaluate * @param node The [AccessibilityNodeInfoCompat] to evaluate
* @return `true` if it is possible to gain accessibility focus * @return `true` if it is possible to gain accessibility focus
*/ */
fun isAccessibilityFocusable(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { private fun isAccessibilityFocusable(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
if (node == null || view == null) { if (node == null || view == null) {
return false return false
} }

View File

@@ -47,6 +47,7 @@ abstract class FragmentCompat<
} }
return field return field
} }
private var hasSupportFragment = false private var hasSupportFragment = false
init { init {
hasSupportFragment = tryGetClassForName("androidx.fragment.app.Fragment") != null hasSupportFragment = tryGetClassForName("androidx.fragment.app.Fragment") != null

View File

@@ -56,7 +56,7 @@ class FragmentCompatFramework :
} }
override fun forFragmentManager(): override fun forFragmentManager():
FragmentManagerAccessorViaReflection<FragmentManager, Fragment>? { FragmentManagerAccessorViaReflection<FragmentManager, Fragment> {
return fragmentManagerAccessor return fragmentManagerAccessor
} }
@@ -75,7 +75,7 @@ class FragmentCompatFramework :
val fragmentManagerAccessor = forFragmentManager() val fragmentManagerAccessor = forFragmentManager()
var addedFragments: List<Any?>? = null var addedFragments: List<Any?>? = null
try { try {
addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager)
} catch (e: Exception) {} } catch (e: Exception) {}
if (addedFragments != null) { if (addedFragments != null) {
@@ -99,7 +99,7 @@ class FragmentCompatFramework :
} }
val activityAccessor = forFragmentActivity() val activityAccessor = forFragmentActivity()
val fragmentManager = activityAccessor?.getFragmentManager(activity) ?: return null val fragmentManager = activityAccessor.getFragmentManager(activity) ?: return null
return findFragmentForViewInFragmentManager(fragmentManager, view) return findFragmentForViewInFragmentManager(fragmentManager, view)
} }
@@ -111,7 +111,7 @@ class FragmentCompatFramework :
val fragmentManagerAccessor = forFragmentManager() val fragmentManagerAccessor = forFragmentManager()
var fragments: List<Any>? = null var fragments: List<Any>? = null
try { try {
fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) fragments = fragmentManagerAccessor.getAddedFragments(fragmentManager)
} catch (e: Exception) {} } catch (e: Exception) {}
if (fragments != null) { if (fragments != null) {

View File

@@ -25,19 +25,19 @@ class FragmentCompatSupportLib :
override val fragmentActivityClass: Class<FragmentActivity> override val fragmentActivityClass: Class<FragmentActivity>
get() = FragmentActivity::class.java get() = FragmentActivity::class.java
override fun forFragment(): FragmentAccessorSupportLib? { override fun forFragment(): FragmentAccessorSupportLib {
return fragmentAccessor return fragmentAccessor
} }
override fun forDialogFragment(): DialogFragmentAccessorSupportLib? { override fun forDialogFragment(): DialogFragmentAccessorSupportLib {
return dialogFragmentAccessor return dialogFragmentAccessor
} }
override fun forFragmentManager(): FragmentManagerAccessor<FragmentManager, Fragment>? { override fun forFragmentManager(): FragmentManagerAccessor<FragmentManager, Fragment> {
return fragmentManagerAccessor return fragmentManagerAccessor
} }
override fun forFragmentActivity(): FragmentActivityAccessorSupportLib? { override fun forFragmentActivity(): FragmentActivityAccessorSupportLib {
return fragmentActivityAccessor return fragmentActivityAccessor
} }
@@ -48,12 +48,12 @@ class FragmentCompatSupportLib :
val activityAccessor = forFragmentActivity() val activityAccessor = forFragmentActivity()
val fragmentManager = val fragmentManager =
activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return emptyList() activityAccessor.getFragmentManager(activity as FragmentActivity) ?: return emptyList()
val fragmentManagerAccessor = forFragmentManager() val fragmentManagerAccessor = forFragmentManager()
var addedFragments: List<Any?>? = null var addedFragments: List<Any?>? = null
try { try {
addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager)
} catch (e: Exception) {} } catch (e: Exception) {}
if (addedFragments != null) { if (addedFragments != null) {
@@ -78,7 +78,7 @@ class FragmentCompatSupportLib :
val activityAccessor = forFragmentActivity() val activityAccessor = forFragmentActivity()
val fragmentManager = val fragmentManager =
activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return null activityAccessor.getFragmentManager(activity as FragmentActivity) ?: return null
return findFragmentForViewInFragmentManager(fragmentManager, view) return findFragmentForViewInFragmentManager(fragmentManager, view)
} }
@@ -90,7 +90,7 @@ class FragmentCompatSupportLib :
val fragmentManagerAccessor = forFragmentManager() val fragmentManagerAccessor = forFragmentManager()
var fragments: List<Any>? = null var fragments: List<Any>? = null
try { try {
fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) fragments = fragmentManagerAccessor.getAddedFragments(fragmentManager)
} catch (e: Exception) {} } catch (e: Exception) {}
if (fragments != null) { if (fragments != null) {
@@ -112,17 +112,13 @@ class FragmentCompatSupportLib :
} }
val fragmentAccessor = forFragment() val fragmentAccessor = forFragment()
fragmentAccessor?.let { accessor -> if (fragmentAccessor.getView(fragment as Fragment) === view) {
if (accessor.getView(fragment as Fragment) === view) { return fragment
return fragment
}
val childFragmentManager = accessor.getChildFragmentManager(fragment as Fragment)
return if (childFragmentManager != null) {
findFragmentForViewInFragmentManager(childFragmentManager, view)
} else null
} }
val childFragmentManager = fragmentAccessor.getChildFragmentManager(fragment as Fragment)
return null return if (childFragmentManager != null) {
findFragmentForViewInFragmentManager(childFragmentManager, view)
} else null
} }
open class FragmentAccessorSupportLib : FragmentAccessor<Fragment, FragmentManager> { open class FragmentAccessorSupportLib : FragmentAccessor<Fragment, FragmentManager> {
@@ -161,7 +157,7 @@ class FragmentCompatSupportLib :
class FragmentActivityAccessorSupportLib : class FragmentActivityAccessorSupportLib :
FragmentActivityAccessor<FragmentActivity, FragmentManager> { FragmentActivityAccessor<FragmentActivity, FragmentManager> {
override fun getFragmentManager(activity: FragmentActivity): FragmentManager? { override fun getFragmentManager(activity: FragmentActivity): FragmentManager {
return activity.supportFragmentManager return activity.supportFragmentManager
} }
} }