diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt index 1ee202859..0bf43ae6f 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/LithoObserver.kt @@ -9,8 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.litho import android.util.Log 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.observers.TreeObserver import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverBuilder import com.facebook.litho.LithoView diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt new file mode 100644 index 000000000..2ca1cc859 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ActivityTracker.kt @@ -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) + fun onActivityStackChanged(stack: List) + fun onActivityDestroyed(activity: Activity, stack: List) + } + + private val activities: MutableList> = mutableListOf() + private val trackedActivities: MutableSet = 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)) + + 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> = 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 + get() { + val stack: MutableList = ArrayList(activities.size) + val activityIterator: MutableIterator> = 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) {} + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt index 75557f141..06dcacfd1 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationRef.kt @@ -9,68 +9,25 @@ package com.facebook.flipper.plugins.uidebugger.core import android.app.Activity import android.app.Application -import android.os.Bundle import android.view.View -import java.lang.ref.WeakReference -class ApplicationRef(val application: Application) : Application.ActivityLifecycleCallbacks { - interface ActivityStackChangedListener { - fun onActivityAdded(activity: Activity, stack: List) - fun onActivityStackChanged(stack: List) - fun onActivityDestroyed(activity: Activity, stack: List) +class ApplicationRef(val application: Application) { + init { + ActivityTracker.start(application) } - val rootsResolver: RootViewResolver - private val activities: MutableList> - private var activityStackChangedlistener: ActivityStackChangedListener? = null - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - activities.add(WeakReference(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> = 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 rootsResolver: RootViewResolver = RootViewResolver() val activitiesStack: List get() { - val stack: MutableList = ArrayList(activities.size) - val activityIterator: MutableIterator> = activities.iterator() - while (activityIterator.hasNext()) { - val activity: Activity? = activityIterator.next().get() - if (activity == null) { - activityIterator.remove() - } else { - stack.add(activity) - } - } - return stack + return ActivityTracker.activitiesStack } val rootViews: List get() { - val roots = rootsResolver.listActiveRootViews() - roots?.let { roots -> - val viewRoots: MutableList = ArrayList(roots.size) + val activeRootViews = rootsResolver.listActiveRootViews() + activeRootViews?.let { roots -> + val viewRoots: MutableList = ArrayList(roots.size) for (root in roots) { viewRoots.add(root.view) } @@ -79,10 +36,4 @@ class ApplicationRef(val application: Application) : Application.ActivityLifecyc return emptyList() } - - init { - rootsResolver = RootViewResolver() - application.registerActivityLifecycleCallbacks(this) - activities = ArrayList>() - } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt new file mode 100644 index 000000000..38d4a6d34 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/FragmentTracker.kt @@ -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? + val frameworkFragment: WeakReference? + 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> = mutableMapOf() + private val viewFragment: MutableMap = 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 { + val key = System.identityHashCode(activity) + val fragments = mutableListOf() + + 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)) } + } + } + } + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt index 5f0edac20..829d5a6e9 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutTraversal.kt @@ -46,7 +46,7 @@ class LayoutTraversal( val childDescriptor = descriptorRegister.descriptorForClassUnsafe(child::class.java).asAny() 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) { stack.add(child) } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt index 6e58fbadc..fbe1195f6 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt @@ -11,7 +11,7 @@ import android.view.View import android.view.ViewGroup /** Layout Visitor traverses the entire view hierarchy from a given root. */ -class LayoutVisitor(val visitor: Visitor) { +class LayoutVisitor(private val visitor: Visitor) { interface Visitor { fun visit(view: View) } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt index f85408a04..20b00e01b 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt @@ -13,7 +13,6 @@ import android.view.WindowManager import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier -import java.util.ArrayList /** * Provides access to all root views in an application. @@ -125,21 +124,23 @@ class RootViewResolver { var params: List? = null try { viewsField?.let { field -> - if (Build.VERSION.SDK_INT < 19) { - val arr = field[windowManagerObj] as Array - views = arr.toList() - } else { - views = field[windowManagerObj] as List - } + views = + if (Build.VERSION.SDK_INT < 19) { + val arr = field[windowManagerObj] as Array + arr.toList() + } else { + field[windowManagerObj] as List + } } paramsField?.let { field -> - if (Build.VERSION.SDK_INT < 19) { - val arr = field[windowManagerObj] as Array - params = arr.toList() as List - } else { - params = field[windowManagerObj] as List - } + params = + if (Build.VERSION.SDK_INT < 19) { + val arr = field[windowManagerObj] as Array + arr.toList() as List + } else { + field[windowManagerObj] as List + } } } catch (re: RuntimeException) { return null @@ -150,12 +151,12 @@ class RootViewResolver { val roots = mutableListOf() views?.let { views -> params?.let { params -> - for (i in views.indices) { - val view = views[i] - // TODO FIX, len(param) is not always the same as len(views) For now just use first - - // params - roots.add(RootView(view, null)) + if (views.size == params.size) { + for (i in views.indices) { + val view = views[i] + val param = params[i] + roots.add(RootView(view, param)) + } } } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ActivityDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ActivityDescriptor.kt index dbf801fae..96478659a 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ActivityDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ActivityDescriptor.kt @@ -9,7 +9,7 @@ package com.facebook.flipper.plugins.uidebugger.descriptors import android.app.Activity 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() { @@ -24,30 +24,12 @@ object ActivityDescriptor : ChainedDescriptor() { override fun onGetChildren(node: Activity, children: MutableList) { node.window?.let { window -> children.add(window) } - var fragments = getDialogFragments(FragmentCompat.supportInstance, node) - for (fragment in fragments) { - children.add(fragment) - } - - fragments = getDialogFragments(FragmentCompat.frameworkInstance, node) - for (fragment in fragments) { - children.add(fragment) - } + val fragments = FragmentTracker.getDialogFragments(node) + fragments.forEach { fragment -> children.add(fragment) } } override fun onGetData( node: Activity, attributeSections: MutableMap ) {} - - private fun getDialogFragments( - compat: FragmentCompat<*, *, *, *>?, - activity: Activity - ): List { - if (compat == null) { - return emptyList() - } - - return compat.getDialogFragments(activity) - } } 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 b3a9d8c77..b9acf0eb5 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 @@ -8,14 +8,11 @@ package com.facebook.flipper.plugins.uidebugger.descriptors import android.app.Activity -import android.util.Log -import com.facebook.flipper.plugins.uidebugger.LogTag +import android.view.View import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef -import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver object ApplicationRefDescriptor : ChainedDescriptor() { - val rootsLocal = RootViewResolver() override fun onGetActiveChild(node: ApplicationRef): Any? { return if (node.activitiesStack.isNotEmpty()) node.activitiesStack.last() else null } @@ -34,27 +31,17 @@ object ApplicationRefDescriptor : ChainedDescriptor() { override fun onGetChildren(node: ApplicationRef, children: MutableList) { 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) - } + val added = mutableSetOf() + for (activity: Activity in node.activitiesStack) { + children.add(activity) + added.add(activity.window.decorView) + } + + // Picks up root views not tied to an activity (dialogs) + for (root in activeRoots) { + if (!added.contains(root)) { + children.add(root) + added.add(root) } } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DescriptorRegister.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DescriptorRegister.kt index ee11b33c0..6508ad326 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DescriptorRegister.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DescriptorRegister.kt @@ -33,17 +33,22 @@ class DescriptorRegister { mapping.register(TextView::class.java, TextViewDescriptor) mapping.register(Button::class.java, ButtonDescriptor) 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) { - val descriptor: NodeDescriptor<*>? = mapping.register[clazz] - descriptor?.let { descriptor -> + val maybeDescriptor: NodeDescriptor<*>? = mapping.register[clazz] + maybeDescriptor?.let { descriptor -> if (descriptor is ChainedDescriptor<*>) { val chainedDescriptor = descriptor as ChainedDescriptor - val superClass: Class<*> = clazz.getSuperclass() - val superDescriptor: NodeDescriptor<*>? = mapping.descriptorForClass(superClass) - // todo we should walk all the way up the superclass hierarchy? - if (superDescriptor is ChainedDescriptor<*>) { - chainedDescriptor.setSuper(superDescriptor as ChainedDescriptor) + val superClass: Class<*> = clazz.superclass + val maybeSuperDescriptor: NodeDescriptor<*>? = mapping.descriptorForClass(superClass) + + maybeSuperDescriptor?.let { superDescriptor -> + if (superDescriptor is ChainedDescriptor<*>) { + chainedDescriptor.setSuper(superDescriptor as ChainedDescriptor) + } } } } @@ -58,11 +63,17 @@ class DescriptorRegister { } fun descriptorForClass(clazz: Class): NodeDescriptor? { - var clazz: Class<*> = clazz - while (!register.containsKey(clazz)) { - clazz = clazz.superclass + var mutableClass: Class<*> = clazz + while (!register.containsKey(mutableClass)) { + mutableClass = mutableClass.superclass + } + + return if (register[clazz] != null) { + @Suppress("UNCHECKED_CAST") + register[clazz] as NodeDescriptor + } else { + null } - return register[clazz] as NodeDescriptor } fun descriptorForClassUnsafe(clazz: Class): NodeDescriptor { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt new file mode 100644 index 000000000..563f2c663 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt @@ -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() { + + 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) { + node.view?.let { view -> children.add(view) } + } + + override fun onGetData( + node: android.app.Fragment, + attributeSections: MutableMap + ) {} +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt new file mode 100644 index 000000000..023de9f86 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt @@ -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() { + + 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) { + node.view?.let { view -> children.add(view) } + } + + override fun onGetData( + node: androidx.fragment.app.Fragment, + attributeSections: MutableMap + ) {} +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt index e6abd27f4..5aca34c42 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt @@ -7,7 +7,6 @@ package com.facebook.flipper.plugins.uidebugger.descriptors -import android.app.Fragment import android.os.Build import android.view.View 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.InspectableObject 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() { @@ -32,8 +31,8 @@ object ViewGroupDescriptor : ChainedDescriptor() { val count = node.childCount - 1 for (i in 0..count) { val child: View = node.getChildAt(i) - val fragment = getAttachedFragmentForView(child) - if (fragment != null && !FragmentCompat.isDialogFragment(fragment)) { + val fragment = FragmentTracker.getFragment(child) + if (fragment != null) { children.add(fragment) } else children.add(child) } @@ -63,19 +62,4 @@ object ViewGroupDescriptor : ChainedDescriptor() { "LAYOUT_MODE_CLIP_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_CLIP_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 - } - } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt index 5e2265608..adef2b362 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/ApplicationTreeObserver.kt @@ -10,13 +10,12 @@ package com.facebook.flipper.plugins.uidebugger.observers import android.util.Log import android.view.View 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.Context 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) */ class ApplicationTreeObserver(val context: Context) : TreeObserver() { @@ -48,6 +47,7 @@ class ApplicationTreeObserver(val context: Context) : TreeObserver { - fun canBuildFor(node: Any): Boolean fun build(context: Context): TreeObserver } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt index b63e3df46..f2dd5084c 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/observers/TreeObserver.kt @@ -5,11 +5,11 @@ * 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 com.facebook.flipper.plugins.uidebugger.LogTag 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. @@ -75,7 +75,7 @@ abstract class TreeObserver { } fun cleanUpRecursive() { - Log.i(LogTag, "Cleaning up observer ${this}") + Log.i(LogTag, "Cleaning up observer $this") children.values.forEach { it.cleanUpRecursive() } unsubscribe() children.clear() diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/AccessibilityUtil.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/AccessibilityUtil.kt index ab8502d42..9657ac587 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/AccessibilityUtil.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/AccessibilityUtil.kt @@ -44,7 +44,7 @@ object AccessibilityUtil { * @param node The [AccessibilityNodeInfoCompat] to evaluate * @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) { return false } @@ -69,7 +69,7 @@ object AccessibilityUtil { * @param node The [AccessibilityNodeInfoCompat] to evaluate * @return `true` if it has any non-actionable speaking descendants within its subtree */ - fun hasNonActionableSpeakingDescendants( + private fun hasNonActionableSpeakingDescendants( node: AccessibilityNodeInfoCompat?, view: View? ): Boolean { @@ -79,10 +79,7 @@ object AccessibilityUtil { val viewGroup = view as ViewGroup val count = viewGroup.childCount - 1 for (i in 0..count) { - val childView = viewGroup.getChildAt(i) - if (childView == null) { - continue - } + val childView = viewGroup.getChildAt(i) ?: continue val childNode = AccessibilityNodeInfoCompat.obtain() try { ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode) @@ -107,7 +104,7 @@ object AccessibilityUtil { * @param node The [AccessibilityNodeInfoCompat] to evaluate * @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) { return false } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompat.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompat.kt index f0b47cc77..c2dbb271d 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompat.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompat.kt @@ -47,6 +47,7 @@ abstract class FragmentCompat< } return field } + private var hasSupportFragment = false init { hasSupportFragment = tryGetClassForName("androidx.fragment.app.Fragment") != null diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatFramework.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatFramework.kt index 5b670a681..a493c6e37 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatFramework.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatFramework.kt @@ -56,7 +56,7 @@ class FragmentCompatFramework : } override fun forFragmentManager(): - FragmentManagerAccessorViaReflection? { + FragmentManagerAccessorViaReflection { return fragmentManagerAccessor } @@ -75,7 +75,7 @@ class FragmentCompatFramework : val fragmentManagerAccessor = forFragmentManager() var addedFragments: List? = null try { - addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager) } catch (e: Exception) {} if (addedFragments != null) { @@ -99,7 +99,7 @@ class FragmentCompatFramework : } val activityAccessor = forFragmentActivity() - val fragmentManager = activityAccessor?.getFragmentManager(activity) ?: return null + val fragmentManager = activityAccessor.getFragmentManager(activity) ?: return null return findFragmentForViewInFragmentManager(fragmentManager, view) } @@ -111,7 +111,7 @@ class FragmentCompatFramework : val fragmentManagerAccessor = forFragmentManager() var fragments: List? = null try { - fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + fragments = fragmentManagerAccessor.getAddedFragments(fragmentManager) } catch (e: Exception) {} if (fragments != null) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatSupportLib.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatSupportLib.kt index aa7b71b15..e40e823cc 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatSupportLib.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatSupportLib.kt @@ -25,19 +25,19 @@ class FragmentCompatSupportLib : override val fragmentActivityClass: Class get() = FragmentActivity::class.java - override fun forFragment(): FragmentAccessorSupportLib? { + override fun forFragment(): FragmentAccessorSupportLib { return fragmentAccessor } - override fun forDialogFragment(): DialogFragmentAccessorSupportLib? { + override fun forDialogFragment(): DialogFragmentAccessorSupportLib { return dialogFragmentAccessor } - override fun forFragmentManager(): FragmentManagerAccessor? { + override fun forFragmentManager(): FragmentManagerAccessor { return fragmentManagerAccessor } - override fun forFragmentActivity(): FragmentActivityAccessorSupportLib? { + override fun forFragmentActivity(): FragmentActivityAccessorSupportLib { return fragmentActivityAccessor } @@ -48,12 +48,12 @@ class FragmentCompatSupportLib : val activityAccessor = forFragmentActivity() val fragmentManager = - activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return emptyList() + activityAccessor.getFragmentManager(activity as FragmentActivity) ?: return emptyList() val fragmentManagerAccessor = forFragmentManager() var addedFragments: List? = null try { - addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager) } catch (e: Exception) {} if (addedFragments != null) { @@ -78,7 +78,7 @@ class FragmentCompatSupportLib : val activityAccessor = forFragmentActivity() val fragmentManager = - activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return null + activityAccessor.getFragmentManager(activity as FragmentActivity) ?: return null return findFragmentForViewInFragmentManager(fragmentManager, view) } @@ -90,7 +90,7 @@ class FragmentCompatSupportLib : val fragmentManagerAccessor = forFragmentManager() var fragments: List? = null try { - fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + fragments = fragmentManagerAccessor.getAddedFragments(fragmentManager) } catch (e: Exception) {} if (fragments != null) { @@ -112,17 +112,13 @@ class FragmentCompatSupportLib : } val fragmentAccessor = forFragment() - fragmentAccessor?.let { accessor -> - if (accessor.getView(fragment as Fragment) === view) { - return fragment - } - val childFragmentManager = accessor.getChildFragmentManager(fragment as Fragment) - return if (childFragmentManager != null) { - findFragmentForViewInFragmentManager(childFragmentManager, view) - } else null + if (fragmentAccessor.getView(fragment as Fragment) === view) { + return fragment } - - return null + val childFragmentManager = fragmentAccessor.getChildFragmentManager(fragment as Fragment) + return if (childFragmentManager != null) { + findFragmentForViewInFragmentManager(childFragmentManager, view) + } else null } open class FragmentAccessorSupportLib : FragmentAccessor { @@ -161,7 +157,7 @@ class FragmentCompatSupportLib : class FragmentActivityAccessorSupportLib : FragmentActivityAccessor { - override fun getFragmentManager(activity: FragmentActivity): FragmentManager? { + override fun getFragmentManager(activity: FragmentActivity): FragmentManager { return activity.supportFragmentManager } }