From ece57689e57fa6de43aa0945dc273d74643def30 Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Thu, 18 Aug 2022 05:16:38 -0700 Subject: [PATCH] Move to public plugins location Summary: ^ So far, we had the 'uidebugger' plugin as a meta-only plugin. This change moves the plugin to OSS space as it will ultimately be its right place. It will also make it easier to iterate on it. The plugin itself is not ready for consumption but at no point we are documenting or integrating it with our samples. Reviewed By: passy Differential Revision: D38742336 fbshipit-source-id: 5cf124722fa7ba75ee9b998c507bfdfb2e4782c1 --- .../uidebugger/UIDebuggerFlipperPlugin.kt | 42 ++++ .../plugins/uidebugger/commands/Command.kt | 30 +++ .../uidebugger/commands/CommandRegister.kt | 18 ++ .../plugins/uidebugger/commands/Context.kt | 10 + .../plugins/uidebugger/commands/GetRoot.kt | 19 ++ .../uidebugger/core/ApplicationObserver.kt | 99 +++++++++ .../plugins/uidebugger/core/LayoutVisitor.kt | 37 ++++ .../uidebugger/core/RootViewResolver.kt | 190 ++++++++++++++++++ .../uidebugger/UIDebuggerFlipperPluginTest.kt | 23 +++ 9 files changed, 468 insertions(+) create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Context.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/GetRoot.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt create mode 100644 android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt create mode 100644 android/src/test/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPluginTest.kt diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt new file mode 100644 index 000000000..0d71e148c --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt @@ -0,0 +1,42 @@ +/* + * 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 + +import com.facebook.flipper.core.FlipperConnection +import com.facebook.flipper.core.FlipperPlugin +import com.facebook.flipper.plugins.uidebugger.commands.CommandRegister +import com.facebook.flipper.plugins.uidebugger.commands.Context +import com.facebook.flipper.plugins.uidebugger.commands.GetRoot + +class UIDebuggerFlipperPlugin : FlipperPlugin { + private var context: Context = Context() + private var connection: FlipperConnection? = null + + override fun getId(): String { + return "UIDebugger" + } + + @Throws(Exception::class) + override fun onConnect(connection: FlipperConnection) { + this.connection = connection + registerCommands(connection) + } + + @Throws(Exception::class) + override fun onDisconnect() { + this.connection = null + } + + override fun runInBackground(): Boolean { + return true + } + + fun registerCommands(connection: FlipperConnection) { + CommandRegister.register(connection, GetRoot(context)) + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.kt new file mode 100644 index 000000000..9e7d7c27b --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Command.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.commands + +import com.facebook.flipper.core.FlipperObject +import com.facebook.flipper.core.FlipperReceiver +import com.facebook.flipper.core.FlipperResponder +import com.facebook.flipper.plugins.common.MainThreadFlipperReceiver + +/** An interface for extensions to the UIDebugger plugin */ +abstract class Command(val context: Context) { + /** The command identifier to respond to */ + abstract fun identifier(): String + /** Execute the command */ + abstract fun execute(params: FlipperObject, response: FlipperResponder) + /** Receiver which is the low-level handler for the incoming request */ + open fun receiver(): FlipperReceiver { + return object : MainThreadFlipperReceiver() { + @kotlin.Throws(java.lang.Exception::class) + override fun onReceiveOnMainThread(params: FlipperObject, responder: FlipperResponder) { + execute(params, responder) + } + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt new file mode 100644 index 000000000..054977382 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/CommandRegister.kt @@ -0,0 +1,18 @@ +/* + * 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.commands + +import com.facebook.flipper.core.FlipperConnection + +sealed class CommandRegister { + companion object { + fun register(connection: FlipperConnection, cmd: T) where T : Command { + connection.receive(cmd.identifier(), cmd.receiver()) + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Context.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Context.kt new file mode 100644 index 000000000..dfef7b373 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/Context.kt @@ -0,0 +1,10 @@ +/* + * 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.commands + +class Context() {} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/GetRoot.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/GetRoot.kt new file mode 100644 index 000000000..d3e17206d --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/commands/GetRoot.kt @@ -0,0 +1,19 @@ +/* + * 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.commands + +import com.facebook.flipper.core.FlipperObject +import com.facebook.flipper.core.FlipperResponder + +class GetRoot(context: Context) : Command(context) { + override fun identifier(): String { + return "getRoot" + } + + override fun execute(params: FlipperObject, response: FlipperResponder) {} +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt new file mode 100644 index 000000000..650e0459d --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/ApplicationObserver.kt @@ -0,0 +1,99 @@ +/* + * 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.app.Application +import android.os.Bundle +import android.view.View +import java.lang.ref.WeakReference + +class ApplicationObserver(val application: Application) : Application.ActivityLifecycleCallbacks { + interface ActivityStackChangedListener { + fun onActivityStackChanged(stack: List) + } + + interface ActivityDestroyedListener { + fun onActivityDestroyed(activity: Activity) + } + + private val rootsResolver: RootViewResolver + private val activities: MutableList> + private var activityStackChangedlistener: ActivityStackChangedListener? = null + private var activityDestroyedListener: ActivityDestroyedListener? = null + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activities.add(WeakReference(activity)) + activityStackChangedlistener?.let { listener -> + listener.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() + } + } + + activityDestroyedListener?.let { listener -> listener.onActivityDestroyed(activity) } + + activityStackChangedlistener?.let { listener -> + listener.onActivityStackChanged(this.activitiesStack) + } + } + + fun setActivityStackChangedListener(listener: ActivityStackChangedListener?) { + activityStackChangedlistener = listener + } + + fun setActivityDestroyedListener(listener: ActivityDestroyedListener?) { + activityDestroyedListener = listener + } + + 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 + } + + val rootViews: List + get() { + val roots = rootsResolver.listActiveRootViews() + roots?.let { roots -> + val viewRoots: MutableList = ArrayList(roots.size) + for (root in roots) { + viewRoots.add(root.view) + } + return viewRoots + } + + return emptyList() + } + + init { + rootsResolver = RootViewResolver() + application.registerActivityLifecycleCallbacks(this) + activities = ArrayList>() + } +} 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 new file mode 100644 index 000000000..6e58fbadc --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/LayoutVisitor.kt @@ -0,0 +1,37 @@ +/* + * 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.view.View +import android.view.ViewGroup + +/** Layout Visitor traverses the entire view hierarchy from a given root. */ +class LayoutVisitor(val visitor: Visitor) { + interface Visitor { + fun visit(view: View) + } + + fun traverse(view: View) { + visitor.visit(view) + + if (view is ViewGroup) { + val viewGroup = view as ViewGroup + val childCount = viewGroup.childCount - 1 + for (i in 0 until childCount) { + val child = viewGroup.getChildAt(i) + traverse(child) + } + } + } + + companion object { + fun create(visitor: Visitor): LayoutVisitor { + return LayoutVisitor(visitor) + } + } +} 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 new file mode 100644 index 000000000..7929033b1 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/RootViewResolver.kt @@ -0,0 +1,190 @@ +/* + * 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.os.Build +import android.view.View +import android.view.WindowManager +import java.lang.reflect.Field +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Modifier +import java.util.ArrayList +import java.util.List + +/** + * Provides access to all root views in an application. + * + * 95% of the time this is unnecessary and we can operate solely on current Activity's root view as + * indicated by getWindow().getDecorView(). However in the case of popup windows, menus, and dialogs + * the actual view hierarchy we should be operating on is not accessible thru public apis. + * + * In the spirit of degrading gracefully when new api levels break compatibility, callers should + * handle a list of size 0 by assuming getWindow().getDecorView() on the currently resumed activity + * is the sole root - this assumption will be correct often enough. + * + * Obviously, you need to be on the main thread to use this. + */ +class RootViewResolver { + private var initialized = false + private var windowManagerObj: Any? = null + private var viewsField: Field? = null + private var paramsField: Field? = null + + class RootView(val view: View, val param: WindowManager.LayoutParams) + interface Listener { + fun onRootViewAdded(rootView: View) + fun onRootViewRemoved(rootView: View) + fun onRootViewsChanged(rootView: List) + } + + class ObservableArrayList() : ArrayList() { + private var listener: Listener? = null + fun setListener(listener: Listener?) { + this.listener = listener + } + + override fun add(value: View): Boolean { + val ret = super.add(value) + listener?.let { l -> + l.onRootViewAdded(value) + l.onRootViewsChanged(this as List) + } + return ret + } + + override fun remove(value: View): Boolean { + val ret = super.remove(value) + listener?.let { l -> + l.onRootViewRemoved(value) + l.onRootViewsChanged(this as List) + } + + return ret + } + + override fun removeAt(index: Int): View { + val view = super.removeAt(index) + listener?.let { l -> + l.onRootViewRemoved(view) + l.onRootViewsChanged(this as List) + } + return view + } + } + + fun attachListener(listener: Listener?) { + if (Build.VERSION.SDK_INT < 19 || listener == null) { + // We don't have a use for this on older APIs. If you do then modify accordingly :) + return + } + if (!initialized) { + initialize() + } + try { + viewsField?.let { vf -> + // Forgive me father for I have sinned... + val modifiers = Field::class.java.getDeclaredField("accessFlags") + modifiers.isAccessible = true + modifiers.setInt(vf, vf.modifiers and Modifier.FINAL.inv()) + + val views = vf[windowManagerObj] as List + + val observableViews = ObservableArrayList() + observableViews.setListener(listener) + observableViews.addAll(views) + + vf[windowManagerObj] = observableViews + } + } catch (e: Throwable) {} + } + + /** + * Lists the active root views in an application at this moment. + * + * @return a list of all the active root views in the application. + * @throws IllegalStateException if invoked from a thread besides the main thread. + */ + fun listActiveRootViews(): List? { + if (!initialized) { + initialize() + } + if (null == windowManagerObj) { + return null + } + if (null == viewsField) { + return null + } + if (null == paramsField) { + return null + } + var views: List? = null + var params: List? = null + try { + viewsField?.let { field -> + if (Build.VERSION.SDK_INT < 19) { + val arr = field[windowManagerObj] as Array + views = arr.toList() as List + } else { + views = 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 + } + } + } catch (re: RuntimeException) { + return null + } catch (iae: IllegalAccessException) { + return null + } + + val roots: ArrayList = ArrayList() + views?.let { views -> + params?.let { params -> + for (i in views.indices) { + roots.add(RootView(views[i], params[i])) + } + } + } + + return roots as List + } + + private fun initialize() { + initialized = true + val accessClass = + if (Build.VERSION.SDK_INT > 16) WINDOW_MANAGER_GLOBAL_CLAZZ else WINDOW_MANAGER_IMPL_CLAZZ + val instanceMethod = if (Build.VERSION.SDK_INT > 16) GET_GLOBAL_INSTANCE else GET_DEFAULT_IMPL + try { + val clazz = Class.forName(accessClass) + val getMethod = clazz.getMethod(instanceMethod) + windowManagerObj = getMethod.invoke(null) + viewsField = clazz.getDeclaredField(VIEWS_FIELD) + viewsField?.let { vf -> vf.setAccessible(true) } + paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD) + paramsField?.let { pf -> pf.setAccessible(true) } + } catch (ite: InvocationTargetException) {} catch (cnfe: ClassNotFoundException) {} catch ( + nsfe: NoSuchFieldException) {} catch (nsme: NoSuchMethodException) {} catch ( + re: RuntimeException) {} catch (iae: IllegalAccessException) {} + } + + companion object { + private val TAG = RootViewResolver::class.java.simpleName + private const val WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl" + private const val WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal" + private const val VIEWS_FIELD = "mViews" + private const val WINDOW_PARAMS_FIELD = "mParams" + private const val GET_DEFAULT_IMPL = "getDefault" + private const val GET_GLOBAL_INSTANCE = "getInstance" + } +} diff --git a/android/src/test/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPluginTest.kt b/android/src/test/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPluginTest.kt new file mode 100644 index 000000000..575dc6772 --- /dev/null +++ b/android/src/test/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPluginTest.kt @@ -0,0 +1,23 @@ +/* + * 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 + +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UIDebuggerFlipperPluginTest { + @Throws(Exception::class) + @Test + fun emptyTest() { + var plugin = UIDebuggerFlipperPlugin() + Assert.assertNotNull(plugin) + } +}