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 new file mode 100644 index 000000000..ab8502d42 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/AccessibilityUtil.kt @@ -0,0 +1,210 @@ +/* + * 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.stetho + +import android.text.TextUtils +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.HorizontalScrollView +import android.widget.ScrollView +import android.widget.Spinner +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat + +/** + * This class provides utility methods for determining certain accessibility properties of [View]s + * and [AccessibilityNodeInfoCompat]s. It is porting some of the checks from + * [com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils], but has stripped many features which + * are unnecessary here. + */ +object AccessibilityUtil { + /** + * Returns whether the specified node has text or a content description. + * + * @param node The node to check. + * @return `true` if the node has text. + */ + fun hasText(node: AccessibilityNodeInfoCompat?): Boolean { + return if (node == null) { + false + } else !TextUtils.isEmpty(node.text) || !TextUtils.isEmpty(node.contentDescription) + } + + /** + * Returns whether the supplied [View] and [AccessibilityNodeInfoCompat] would produce spoken + * feedback if it were accessibility focused. NOTE: not all speaking nodes are focusable. + * + * @param view The [View] to evaluate + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if it meets the criterion for producing spoken feedback + */ + fun isSpeakingNode(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { + if (node == null || view == null) { + return false + } + if (!node.isVisibleToUser) { + return false + } + val important = ViewCompat.getImportantForAccessibility(view) + return if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS || + important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO && node.childCount <= 0) { + false + } else node.isCheckable || hasText(node) || hasNonActionableSpeakingDescendants(node, view) + } + + /** + * Determines if the supplied [View] and [AccessibilityNodeInfoCompat] has any children which are + * not independently accessibility focusable and also have a spoken description. + * + * NOTE: Accessibility services will include these children's descriptions in the closest + * focusable ancestor. + * + * @param view The [View] to evaluate + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if it has any non-actionable speaking descendants within its subtree + */ + fun hasNonActionableSpeakingDescendants( + node: AccessibilityNodeInfoCompat?, + view: View? + ): Boolean { + if (node == null || view == null || view !is ViewGroup) { + return false + } + 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 childNode = AccessibilityNodeInfoCompat.obtain() + try { + ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode) + if (isAccessibilityFocusable(childNode, childView)) { + continue + } + if (isSpeakingNode(childNode, childView)) { + return true + } + } finally { + childNode.recycle() + } + } + return false + } + + /** + * Determines if the provided [View] and [AccessibilityNodeInfoCompat] meet the criteria for + * gaining accessibility focus. + * + * @param view The [View] to evaluate + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if it is possible to gain accessibility focus + */ + fun isAccessibilityFocusable(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { + if (node == null || view == null) { + return false + } + + // Never focus invisible nodes. + if (!node.isVisibleToUser) { + return false + } + + // Always focus "actionable" nodes. + return if (isActionableForAccessibility(node)) { + true + } else isTopLevelScrollItem(node, view) && isSpeakingNode(node, view) + // only focus top-level list items with non-actionable speaking children. + } + + /** + * Determines whether the provided [View] and [AccessibilityNodeInfoCompat] is a top-level item in + * a scrollable container. + * + * @param view The [View] to evaluate + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if it is a top-level item in a scrollable container. + */ + fun isTopLevelScrollItem(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { + if (node == null || view == null) { + return false + } + val parent = ViewCompat.getParentForAccessibility(view) as View? ?: return false + if (node.isScrollable) { + return true + } + val actionList: List<*> = node.actionList + if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) || + actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) { + return true + } + + // AdapterView, ScrollView, and HorizontalScrollView are focusable + // containers, but Spinner is a special case. + return if (parent is Spinner) { + false + } else parent is AdapterView<*> || parent is ScrollView || parent is HorizontalScrollView + } + + /** + * Returns whether a node is actionable. That is, the node supports one of + * [AccessibilityNodeInfoCompat.isClickable], [AccessibilityNodeInfoCompat.isFocusable], or + * [AccessibilityNodeInfoCompat.isLongClickable]. + * + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if node is actionable. + */ + fun isActionableForAccessibility(node: AccessibilityNodeInfoCompat?): Boolean { + if (node == null) { + return false + } + if (node.isClickable || node.isLongClickable || node.isFocusable) { + return true + } + val actionList: List<*> = node.actionList + return actionList.contains(AccessibilityNodeInfoCompat.ACTION_CLICK) || + actionList.contains(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK) || + actionList.contains(AccessibilityNodeInfoCompat.ACTION_FOCUS) + } + + /** + * Determines if any of the provided [View]'s and [AccessibilityNodeInfoCompat]'s ancestors can + * receive accessibility focus + * + * @param view The [View] to evaluate + * @param node The [AccessibilityNodeInfoCompat] to evaluate + * @return `true` if an ancestor of may receive accessibility focus + */ + fun hasFocusableAncestor(node: AccessibilityNodeInfoCompat?, view: View?): Boolean { + if (node == null || view == null) { + return false + } + val parentView = ViewCompat.getParentForAccessibility(view) + if (parentView !is View) { + return false + } + val parentNode = AccessibilityNodeInfoCompat.obtain() + try { + ViewCompat.onInitializeAccessibilityNodeInfo((parentView as View), parentNode) + if (parentNode == null) { + return false + } + if (isAccessibilityFocusable(parentNode, parentView as View)) { + return true + } + if (hasFocusableAncestor(parentNode, parentView as View)) { + return true + } + } finally { + parentNode!!.recycle() + } + return false + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/DialogFragmentAccessor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/DialogFragmentAccessor.kt new file mode 100644 index 000000000..1cfa62dd3 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/DialogFragmentAccessor.kt @@ -0,0 +1,15 @@ +/* + * 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.stetho + +import android.app.Dialog + +interface DialogFragmentAccessor : + FragmentAccessor { + fun getDialog(dialogFragment: DIALOG_FRAGMENT): Dialog? +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentAccessor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentAccessor.kt new file mode 100644 index 000000000..00e1db5fe --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentAccessor.kt @@ -0,0 +1,24 @@ +/* + * 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.stetho + +import android.content.res.Resources +import android.view.View + +interface FragmentAccessor { + fun getFragmentManager(fragment: FRAGMENT): FRAGMENT_MANAGER? + fun getResources(fragment: FRAGMENT): Resources? + fun getId(fragment: FRAGMENT): Int + fun getTag(fragment: FRAGMENT): String? + fun getView(fragment: FRAGMENT): View? + fun getChildFragmentManager(fragment: FRAGMENT): FRAGMENT_MANAGER? + + companion object { + const val NO_ID = 0 + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentActivityAccessor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentActivityAccessor.kt new file mode 100644 index 000000000..4671ffa69 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentActivityAccessor.kt @@ -0,0 +1,14 @@ +/* + * 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.stetho + +import android.app.Activity + +interface FragmentActivityAccessor { + fun getFragmentManager(activity: Activity): FRAGMENT_MANAGER? +} 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 new file mode 100644 index 000000000..a06c2aa69 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompat.kt @@ -0,0 +1,134 @@ +/* + * 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.stetho + +import android.app.Activity +import android.view.View +import com.facebook.flipper.plugins.uidebugger.stetho.ReflectionUtil.getFieldValue +import com.facebook.flipper.plugins.uidebugger.stetho.ReflectionUtil.tryGetClassForName +import java.lang.reflect.Field +import javax.annotation.concurrent.NotThreadSafe + +/** + * Compatibility abstraction which allows us to generalize access to both the support library's + * fragments and the built-in framework version. Note: both versions can be live at the same time in + * a single application and even on a single object instance. + * + * Type safety is enforced via generics internal to the implementation but treated as opaque from + * the outside. + * + * @param + * @param + * @param + * @param + */ +@NotThreadSafe +abstract class FragmentCompat< + FRAGMENT, DIALOG_FRAGMENT, FRAGMENT_MANAGER, FRAGMENT_ACTIVITY : Activity> { + + companion object { + var frameworkInstance: FragmentCompat<*, *, *, *>? = null + get() { + if (field == null) { + field = FragmentCompatFramework() + } + return field + } + + var supportInstance: FragmentCompat<*, *, *, *>? = null + get() { + if (field == null && hasSupportFragment) { + field = FragmentCompatSupportLib() + } + return field + } + private var hasSupportFragment = false + init { + hasSupportFragment = tryGetClassForName("androidx.fragment.app.Fragment") != null + } + + fun isDialogFragment(fragment: Any): Boolean { + val supportLib: FragmentCompat<*, *, *, *>? = supportInstance + if (supportLib != null && supportLib.dialogFragmentClass?.isInstance(fragment) == true) { + return true + } + + val framework: FragmentCompat<*, *, *, *>? = frameworkInstance + return framework != null && framework.dialogFragmentClass?.isInstance(fragment) == true + } + + fun findFragmentForView(view: View): Any? { + val activity = ViewUtil.tryGetActivity(view) ?: return null + return findFragmentForViewInActivity(activity, view) + } + + private fun findFragmentForViewInActivity(activity: Activity, view: View): Any? { + val supportLib: FragmentCompat<*, *, *, *>? = supportInstance + + // Try the support library version if it is present and the activity is FragmentActivity. + if (supportLib != null && supportLib.fragmentActivityClass?.isInstance(activity) == true) { + val fragment = supportLib.findFragmentForViewInActivity(activity, view) + if (fragment != null) { + return fragment + } + } + + // Try the actual Android runtime version if we are on a sufficiently high API level for it to + // exist. Note that technically we can have both the support library and the framework + // version in the same object instance due to FragmentActivity extending Activity (which has + // fragment support in the system). + val framework: FragmentCompat<*, *, *, *>? = frameworkInstance + if (framework != null) { + val fragment = framework.findFragmentForViewInActivity(activity, view) + if (fragment != null) { + return fragment + } + } + return null + } + } + + abstract val fragmentClass: Class? + abstract val dialogFragmentClass: Class? + abstract val fragmentActivityClass: Class? + + abstract fun forFragment(): FragmentAccessor? + abstract fun forDialogFragment(): + DialogFragmentAccessor? + abstract fun forFragmentManager(): FragmentManagerAccessor? + abstract fun forFragmentActivity(): FragmentActivityAccessor? + + abstract fun getFragments(activity: Activity): List + abstract fun findFragmentForViewInActivity(activity: Activity, view: View): Any? + abstract fun findFragmentForViewInFragment(fragment: Any, view: View): Any? + + class FragmentManagerAccessorViaReflection : + FragmentManagerAccessor { + private var fieldMAdded: Field? = null + + override fun getAddedFragments(fragmentManager: FRAGMENT_MANAGER): List { + + // This field is actually sitting on FragmentManagerImpl, which derives from FragmentManager + if (fieldMAdded == null) { + fragmentManager?.let { manager -> + val field = manager::class.java.getDeclaredField("mAdded") + if (field != null) { + field.isAccessible = true + fieldMAdded = field + } + } + } + + fieldMAdded?.let { field -> + return getFieldValue(field, fragmentManager) as List + } + + return emptyList() + } + } +} 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 new file mode 100644 index 000000000..cbbba5eb4 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatFramework.kt @@ -0,0 +1,218 @@ +/* + * 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.stetho + +import android.annotation.TargetApi +import android.app.* +import android.app.Dialog +import android.app.DialogFragment +import android.app.Fragment +import android.app.FragmentManager +import android.content.res.Resources +import android.os.Build +import android.view.View + +class FragmentCompatFramework : + FragmentCompat() { + companion object { + private var fragmentAccessor: FragmentAccessorFrameworkHoneycomb? = null + private var dialogFragmentAccessor: DialogFragmentAccessorFramework? = null + private val fragmentManagerAccessor = + FragmentManagerAccessorViaReflection() + private val fragmentActivityAccessor = FragmentActivityAccessorFramework() + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + fragmentAccessor = FragmentAccessorFrameworkJellyBean() + } else { + fragmentAccessor = FragmentAccessorFrameworkHoneycomb() + } + fragmentAccessor?.let { accessor -> + dialogFragmentAccessor = DialogFragmentAccessorFramework(accessor) + } + } + } + + override val fragmentClass: Class + get() = Fragment::class.java + + override val dialogFragmentClass: Class + get() = DialogFragment::class.java + + override val fragmentActivityClass: Class + get() = Activity::class.java + + override fun forFragment(): FragmentAccessorFrameworkHoneycomb? { + return fragmentAccessor + } + + override fun forDialogFragment(): DialogFragmentAccessorFramework? { + return dialogFragmentAccessor + } + + override fun forFragmentManager(): + FragmentManagerAccessorViaReflection? { + return fragmentManagerAccessor + } + + override fun forFragmentActivity(): FragmentActivityAccessorFramework { + return fragmentActivityAccessor + } + + override fun getFragments(activity: Activity): List { + if (!fragmentActivityClass.isInstance(activity)) { + return emptyList() + } + + val activityAccessor = forFragmentActivity() + val fragmentManager = activityAccessor.getFragmentManager(activity) ?: return emptyList() + + val fragmentManagerAccessor = forFragmentManager() + var addedFragments: List? = null + try { + addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + } catch (e: Exception) {} + + if (addedFragments != null) { + val N = addedFragments.size - 1 + val dialogFragments = mutableListOf() + for (i in 0..N) { + val fragment = addedFragments[i] + dialogFragmentClass?.isInstance(fragment)?.let { fragment -> dialogFragments.add(fragment) } + } + return dialogFragments + } + + return emptyList() + } + + override fun findFragmentForViewInActivity(activity: Activity, view: View): Any? { + if (!fragmentActivityClass.isInstance(activity)) { + return null + } + + val activityAccessor = forFragmentActivity() + val fragmentManager = activityAccessor?.getFragmentManager(activity) ?: return null + + return findFragmentForViewInFragmentManager(fragmentManager, view) + } + + private fun findFragmentForViewInFragmentManager( + fragmentManager: FragmentManager, + view: View + ): Any? { + val fragmentManagerAccessor = forFragmentManager() + var fragments: List? = null + try { + fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + } catch (e: Exception) {} + + if (fragments != null) { + val N = fragments.size - 1 + for (i in 0..N) { + val fragment = fragments[i] + val result = findFragmentForViewInFragment(fragment, view) + if (result != null) { + return result + } + } + } + return null + } + + override fun findFragmentForViewInFragment(fragment: Any, view: View): Any? { + if (!fragmentClass.isInstance(fragment)) { + return null + } + + 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 + } + + return null + } + + open class FragmentAccessorFrameworkHoneycomb : FragmentAccessor { + override fun getFragmentManager(fragment: Fragment): FragmentManager? { + return fragment.fragmentManager + } + + override fun getResources(fragment: Fragment): Resources { + return fragment.resources + } + + override fun getId(fragment: Fragment): Int { + return fragment.id + } + + override fun getTag(fragment: Fragment): String? { + return fragment.tag + } + + override fun getView(fragment: Fragment): View? { + return fragment.view + } + + override fun getChildFragmentManager(fragment: Fragment): FragmentManager? { + return null + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + class FragmentAccessorFrameworkJellyBean : FragmentAccessorFrameworkHoneycomb() { + override fun getChildFragmentManager(fragment: Fragment): FragmentManager? { + return fragment.childFragmentManager + } + } + + class DialogFragmentAccessorFramework( + val fragmentAccessor: FragmentAccessor + ) : DialogFragmentAccessor { + + override fun getDialog(dialogFragment: DialogFragment): Dialog { + return dialogFragment.dialog + } + + override fun getFragmentManager(fragment: Fragment): FragmentManager? { + return fragmentAccessor.getFragmentManager(fragment) + } + + override fun getResources(fragment: Fragment): Resources? { + return fragmentAccessor.getResources(fragment) + } + + override fun getId(fragment: Fragment): Int { + return fragmentAccessor.getId(fragment) + } + + override fun getTag(fragment: Fragment): String? { + return fragmentAccessor.getTag(fragment) + } + + override fun getView(fragment: Fragment): View? { + return fragmentAccessor.getView(fragment) + } + + override fun getChildFragmentManager(fragment: Fragment): FragmentManager? { + return fragmentAccessor.getChildFragmentManager(fragment) + } + } + + class FragmentActivityAccessorFramework : FragmentActivityAccessor { + override fun getFragmentManager(activity: Activity): FragmentManager? { + return activity.fragmentManager + } + } +} 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 new file mode 100644 index 000000000..dcda39127 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentCompatSupportLib.kt @@ -0,0 +1,174 @@ +/* + * 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.stetho + +import android.app.Activity +import android.app.Dialog +import android.content.res.Resources +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager + +class FragmentCompatSupportLib : + FragmentCompat() { + override val fragmentClass: Class + get() = Fragment::class.java + override val dialogFragmentClass: Class + get() = DialogFragment::class.java + override val fragmentActivityClass: Class + get() = FragmentActivity::class.java + + override fun forFragment(): FragmentAccessorSupportLib? { + return fragmentAccessor + } + + override fun forDialogFragment(): DialogFragmentAccessorSupportLib? { + return dialogFragmentAccessor + } + + override fun forFragmentManager(): FragmentManagerAccessor? { + return fragmentManagerAccessor + } + + override fun forFragmentActivity(): FragmentActivityAccessorSupportLib? { + return fragmentActivityAccessor + } + + override fun getFragments(activity: Activity): List { + if (!fragmentActivityClass.isInstance(activity)) { + return emptyList() + } + + val activityAccessor = forFragmentActivity() + val fragmentManager = + activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return emptyList() + + val fragmentManagerAccessor = forFragmentManager() + var addedFragments: List? = null + try { + addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + } catch (e: Exception) {} + + if (addedFragments != null) { + val N = addedFragments.size - 1 + val dialogFragments = mutableListOf() + for (i in 0..N) { + val fragment = addedFragments[i] + dialogFragmentClass?.isInstance(fragment)?.let { fragment -> dialogFragments.add(fragment) } + } + return dialogFragments + } + + return emptyList() + } + + override fun findFragmentForViewInActivity(activity: Activity, view: View): Any? { + if (!fragmentActivityClass.isInstance(activity)) { + return null + } + + val activityAccessor = forFragmentActivity() + val fragmentManager = + activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return null + + return findFragmentForViewInFragmentManager(fragmentManager, view) + } + + private fun findFragmentForViewInFragmentManager( + fragmentManager: FragmentManager, + view: View + ): Any? { + val fragmentManagerAccessor = forFragmentManager() + var fragments: List? = null + try { + fragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager) + } catch (e: Exception) {} + + if (fragments != null) { + val N = fragments.size - 1 + for (i in 0..N) { + val fragment = fragments[i] + val result = findFragmentForViewInFragment(fragment, view) + if (result != null) { + return result + } + } + } + return null + } + + override fun findFragmentForViewInFragment(fragment: Any, view: View): Any? { + if (!fragmentClass.isInstance(fragment)) { + return null + } + + 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 + } + + return null + } + + open class FragmentAccessorSupportLib : FragmentAccessor { + override fun getFragmentManager(fragment: Fragment): FragmentManager? { + return fragment.fragmentManager + } + + override fun getResources(fragment: Fragment): Resources? { + return fragment.resources + } + + override fun getId(fragment: Fragment): Int { + return fragment.id + } + + override fun getTag(fragment: Fragment): String? { + return fragment.tag + } + + override fun getView(fragment: Fragment): View? { + return fragment.view + } + + override fun getChildFragmentManager(fragment: Fragment): FragmentManager? { + return fragment.childFragmentManager + } + } + + class DialogFragmentAccessorSupportLib : + FragmentAccessorSupportLib(), + DialogFragmentAccessor { + override fun getDialog(dialogFragment: DialogFragment): Dialog? { + return dialogFragment.dialog + } + } + + class FragmentActivityAccessorSupportLib : + FragmentActivityAccessor { + override fun getFragmentManager(activity: FragmentActivity): FragmentManager? { + return activity.supportFragmentManager + } + } + + companion object { + private val fragmentAccessor = FragmentAccessorSupportLib() + private val dialogFragmentAccessor = DialogFragmentAccessorSupportLib() + private val fragmentManagerAccessor = + FragmentManagerAccessorViaReflection() + private val fragmentActivityAccessor = FragmentActivityAccessorSupportLib() + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentManagerAccessor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentManagerAccessor.kt new file mode 100644 index 000000000..e1747037b --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/FragmentManagerAccessor.kt @@ -0,0 +1,12 @@ +/* + * 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.stetho + +interface FragmentManagerAccessor { + fun getAddedFragments(fragmentManager: FRAGMENT_MANAGER): List +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ReflectionUtil.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ReflectionUtil.kt new file mode 100644 index 000000000..92e9c3252 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ReflectionUtil.kt @@ -0,0 +1,36 @@ +/* + * 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.stetho + +import java.lang.reflect.Field + +object ReflectionUtil { + inline fun tryGetClassForName(className: String): Class<*>? { + return try { + Class.forName(className) + } catch (e: ClassNotFoundException) { + null + } + } + + inline fun tryGetDeclaredField(theClass: Class<*>, fieldName: String): Field? { + return try { + theClass.getDeclaredField(fieldName) + } catch (e: NoSuchFieldException) { + null + } + } + + inline fun getFieldValue(field: Field, target: Any?): Any? { + return try { + field[target] + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ResourcesUtil.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ResourcesUtil.kt new file mode 100644 index 000000000..120164762 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ResourcesUtil.kt @@ -0,0 +1,63 @@ +/* + * 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.stetho + +import android.content.res.Resources +import android.content.res.Resources.NotFoundException +import javax.annotation.Nonnull + +object ResourcesUtil { + @Nonnull + fun getIdStringQuietly(idContext: Any, r: Resources?, resourceId: Int): String { + try { + return getIdString(r, resourceId) + } catch (e: NotFoundException) { + val idString = getFallbackIdString(resourceId) + return idString + } + } + + @Throws(NotFoundException::class) + fun getIdString(r: Resources?, resourceId: Int): String { + if (r == null) { + return getFallbackIdString(resourceId) + } + val prefix: String + val prefixSeparator: String + when (getResourcePackageId(resourceId)) { + 0x7f -> { + prefix = "" + prefixSeparator = "" + } + else -> { + prefix = r.getResourcePackageName(resourceId) + prefixSeparator = ":" + } + } + val typeName = r.getResourceTypeName(resourceId) + val entryName = r.getResourceEntryName(resourceId) + val sb = + StringBuilder( + 1 + prefix.length + prefixSeparator.length + typeName.length + 1 + entryName.length) + sb.append("@") + sb.append(prefix) + sb.append(prefixSeparator) + sb.append(typeName) + sb.append("/") + sb.append(entryName) + return sb.toString() + } + + private fun getFallbackIdString(resourceId: Int): String { + return "#" + Integer.toHexString(resourceId) + } + + private fun getResourcePackageId(id: Int): Int { + return (id ushr 24) and 0xff + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ViewUtil.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ViewUtil.kt new file mode 100644 index 000000000..b1bcf3e00 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/stetho/ViewUtil.kt @@ -0,0 +1,47 @@ +/* + * 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.stetho + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.view.View + +object ViewUtil { + fun tryGetActivity(view: View?): Activity? { + if (view == null) { + return null + } + val context = view.context + val activityFromContext = tryGetActivity(context) + if (activityFromContext != null) { + return activityFromContext + } + val parent = view.parent + if (parent is View) { + val parentView = parent as View + return tryGetActivity(parentView) + } + return null + } + + private fun tryGetActivity(context: Context): Activity? { + var context: Context? = context + while (context != null) { + context = + if (context is Activity) { + return context + } else if (context is ContextWrapper) { + context.baseContext + } else { + return null + } + } + return null + } +}