Stetho-bits
Summary: ^ From: https://github.com/facebook/stetho The copies used by the original plugin were located at: https://www.internalfb.com/code/fbsource/[a8a0c1be09a9]/xplat/sonar/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/stethocopies/ In addition, copies were translated to Kotlin, and adjusted accordingly. Reviewed By: LukeDefeo Differential Revision: D38783216 fbshipit-source-id: 2375bd94d333a2edb6156602b47dee32c83472ff
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ece57689e5
commit
543ea489db
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<DIALOG_FRAGMENT, FRAGMENT, FRAGMENT_MANAGER> :
|
||||||
|
FragmentAccessor<FRAGMENT, FRAGMENT_MANAGER> {
|
||||||
|
fun getDialog(dialogFragment: DIALOG_FRAGMENT): Dialog?
|
||||||
|
}
|
||||||
@@ -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<FRAGMENT, FRAGMENT_MANAGER> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Activity, FRAGMENT_MANAGER> {
|
||||||
|
fun getFragmentManager(activity: Activity): FRAGMENT_MANAGER?
|
||||||
|
}
|
||||||
@@ -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 <FRAGMENT>
|
||||||
|
* @param <DIALOG_FRAGMENT>
|
||||||
|
* @param <FRAGMENT_MANAGER>
|
||||||
|
* @param <FRAGMENT_ACTIVITY>
|
||||||
|
*/
|
||||||
|
@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<FRAGMENT>?
|
||||||
|
abstract val dialogFragmentClass: Class<DIALOG_FRAGMENT>?
|
||||||
|
abstract val fragmentActivityClass: Class<FRAGMENT_ACTIVITY>?
|
||||||
|
|
||||||
|
abstract fun forFragment(): FragmentAccessor<FRAGMENT, FRAGMENT_MANAGER>?
|
||||||
|
abstract fun forDialogFragment():
|
||||||
|
DialogFragmentAccessor<DIALOG_FRAGMENT, FRAGMENT, FRAGMENT_MANAGER>?
|
||||||
|
abstract fun forFragmentManager(): FragmentManagerAccessor<FRAGMENT_MANAGER, FRAGMENT>?
|
||||||
|
abstract fun forFragmentActivity(): FragmentActivityAccessor<FRAGMENT_ACTIVITY, FRAGMENT_MANAGER>?
|
||||||
|
|
||||||
|
abstract fun getFragments(activity: Activity): List<Any>
|
||||||
|
abstract fun findFragmentForViewInActivity(activity: Activity, view: View): Any?
|
||||||
|
abstract fun findFragmentForViewInFragment(fragment: Any, view: View): Any?
|
||||||
|
|
||||||
|
class FragmentManagerAccessorViaReflection<FRAGMENT_MANAGER, FRAGMENT> :
|
||||||
|
FragmentManagerAccessor<FRAGMENT_MANAGER, FRAGMENT> {
|
||||||
|
private var fieldMAdded: Field? = null
|
||||||
|
|
||||||
|
override fun getAddedFragments(fragmentManager: FRAGMENT_MANAGER): List<FRAGMENT> {
|
||||||
|
|
||||||
|
// 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<FRAGMENT>
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Fragment, DialogFragment, FragmentManager, Activity>() {
|
||||||
|
companion object {
|
||||||
|
private var fragmentAccessor: FragmentAccessorFrameworkHoneycomb? = null
|
||||||
|
private var dialogFragmentAccessor: DialogFragmentAccessorFramework? = null
|
||||||
|
private val fragmentManagerAccessor =
|
||||||
|
FragmentManagerAccessorViaReflection<FragmentManager, Fragment>()
|
||||||
|
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<Fragment>
|
||||||
|
get() = Fragment::class.java
|
||||||
|
|
||||||
|
override val dialogFragmentClass: Class<DialogFragment>
|
||||||
|
get() = DialogFragment::class.java
|
||||||
|
|
||||||
|
override val fragmentActivityClass: Class<Activity>
|
||||||
|
get() = Activity::class.java
|
||||||
|
|
||||||
|
override fun forFragment(): FragmentAccessorFrameworkHoneycomb? {
|
||||||
|
return fragmentAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forDialogFragment(): DialogFragmentAccessorFramework? {
|
||||||
|
return dialogFragmentAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forFragmentManager():
|
||||||
|
FragmentManagerAccessorViaReflection<FragmentManager, Fragment>? {
|
||||||
|
return fragmentManagerAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forFragmentActivity(): FragmentActivityAccessorFramework {
|
||||||
|
return fragmentActivityAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFragments(activity: Activity): List<Any> {
|
||||||
|
if (!fragmentActivityClass.isInstance(activity)) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val activityAccessor = forFragmentActivity()
|
||||||
|
val fragmentManager = activityAccessor.getFragmentManager(activity) ?: return emptyList()
|
||||||
|
|
||||||
|
val fragmentManagerAccessor = forFragmentManager()
|
||||||
|
var addedFragments: List<Any?>? = null
|
||||||
|
try {
|
||||||
|
addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
if (addedFragments != null) {
|
||||||
|
val N = addedFragments.size - 1
|
||||||
|
val dialogFragments = mutableListOf<Any>()
|
||||||
|
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<Any>? = 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<Fragment, FragmentManager> {
|
||||||
|
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<Fragment, FragmentManager>
|
||||||
|
) : DialogFragmentAccessor<DialogFragment, Fragment, FragmentManager> {
|
||||||
|
|
||||||
|
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<Activity, FragmentManager> {
|
||||||
|
override fun getFragmentManager(activity: Activity): FragmentManager? {
|
||||||
|
return activity.fragmentManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Fragment, DialogFragment, FragmentManager, FragmentActivity>() {
|
||||||
|
override val fragmentClass: Class<Fragment>
|
||||||
|
get() = Fragment::class.java
|
||||||
|
override val dialogFragmentClass: Class<DialogFragment>
|
||||||
|
get() = DialogFragment::class.java
|
||||||
|
override val fragmentActivityClass: Class<FragmentActivity>
|
||||||
|
get() = FragmentActivity::class.java
|
||||||
|
|
||||||
|
override fun forFragment(): FragmentAccessorSupportLib? {
|
||||||
|
return fragmentAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forDialogFragment(): DialogFragmentAccessorSupportLib? {
|
||||||
|
return dialogFragmentAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forFragmentManager(): FragmentManagerAccessor<FragmentManager, Fragment>? {
|
||||||
|
return fragmentManagerAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun forFragmentActivity(): FragmentActivityAccessorSupportLib? {
|
||||||
|
return fragmentActivityAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFragments(activity: Activity): List<Any> {
|
||||||
|
if (!fragmentActivityClass.isInstance(activity)) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val activityAccessor = forFragmentActivity()
|
||||||
|
val fragmentManager =
|
||||||
|
activityAccessor?.getFragmentManager(activity as FragmentActivity) ?: return emptyList()
|
||||||
|
|
||||||
|
val fragmentManagerAccessor = forFragmentManager()
|
||||||
|
var addedFragments: List<Any?>? = null
|
||||||
|
try {
|
||||||
|
addedFragments = fragmentManagerAccessor?.getAddedFragments(fragmentManager)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
if (addedFragments != null) {
|
||||||
|
val N = addedFragments.size - 1
|
||||||
|
val dialogFragments = mutableListOf<Any>()
|
||||||
|
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<Any>? = 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<Fragment, FragmentManager> {
|
||||||
|
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<DialogFragment, Fragment, FragmentManager> {
|
||||||
|
override fun getDialog(dialogFragment: DialogFragment): Dialog? {
|
||||||
|
return dialogFragment.dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragmentActivityAccessorSupportLib :
|
||||||
|
FragmentActivityAccessor<FragmentActivity, FragmentManager> {
|
||||||
|
override fun getFragmentManager(activity: FragmentActivity): FragmentManager? {
|
||||||
|
return activity.supportFragmentManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val fragmentAccessor = FragmentAccessorSupportLib()
|
||||||
|
private val dialogFragmentAccessor = DialogFragmentAccessorSupportLib()
|
||||||
|
private val fragmentManagerAccessor =
|
||||||
|
FragmentManagerAccessorViaReflection<FragmentManager, Fragment>()
|
||||||
|
private val fragmentActivityAccessor = FragmentActivityAccessorSupportLib()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FRAGMENT_MANAGER, FRAGMENT> {
|
||||||
|
fun getAddedFragments(fragmentManager: FRAGMENT_MANAGER): List<FRAGMENT>
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user