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
This commit is contained in:
Lorenzo Blasa
2022-08-18 05:16:38 -07:00
committed by Facebook GitHub Bot
parent 3b8e74d16f
commit ece57689e5
9 changed files with 468 additions and 0 deletions

View File

@@ -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))
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.flipper.plugins.uidebugger.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)
}
}
}
}

View File

@@ -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 <T> register(connection: FlipperConnection, cmd: T) where T : Command {
connection.receive(cmd.identifier(), cmd.receiver())
}
}
}

View File

@@ -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() {}

View File

@@ -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) {}
}

View File

@@ -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<Activity>)
}
interface ActivityDestroyedListener {
fun onActivityDestroyed(activity: Activity)
}
private val rootsResolver: RootViewResolver
private val activities: MutableList<WeakReference<Activity>>
private var activityStackChangedlistener: ActivityStackChangedListener? = null
private var activityDestroyedListener: ActivityDestroyedListener? = null
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities.add(WeakReference<Activity>(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<WeakReference<Activity>> = 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<Activity>
get() {
val stack: MutableList<Activity> = ArrayList<Activity>(activities.size)
val activityIterator: MutableIterator<WeakReference<Activity>> = activities.iterator()
while (activityIterator.hasNext()) {
val activity: Activity? = activityIterator.next().get()
if (activity == null) {
activityIterator.remove()
} else {
stack.add(activity)
}
}
return stack
}
val rootViews: List<View>
get() {
val roots = rootsResolver.listActiveRootViews()
roots?.let { roots ->
val viewRoots: MutableList<View> = ArrayList<View>(roots.size)
for (root in roots) {
viewRoots.add(root.view)
}
return viewRoots
}
return emptyList()
}
init {
rootsResolver = RootViewResolver()
application.registerActivityLifecycleCallbacks(this)
activities = ArrayList<WeakReference<Activity>>()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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<View>)
}
class ObservableArrayList() : ArrayList<View>() {
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<View>)
}
return ret
}
override fun remove(value: View): Boolean {
val ret = super.remove(value)
listener?.let { l ->
l.onRootViewRemoved(value)
l.onRootViewsChanged(this as List<View>)
}
return ret
}
override fun removeAt(index: Int): View {
val view = super.removeAt(index)
listener?.let { l ->
l.onRootViewRemoved(view)
l.onRootViewsChanged(this as List<View>)
}
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<View>
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<RootView>? {
if (!initialized) {
initialize()
}
if (null == windowManagerObj) {
return null
}
if (null == viewsField) {
return null
}
if (null == paramsField) {
return null
}
var views: List<View>? = null
var params: List<WindowManager.LayoutParams>? = null
try {
viewsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) {
val arr = field[windowManagerObj] as Array<View>
views = arr.toList() as List<View>
} else {
views = field[windowManagerObj] as List<View>
}
}
paramsField?.let { field ->
if (Build.VERSION.SDK_INT < 19) {
val arr = field[windowManagerObj] as Array<WindowManager.LayoutParams>
params = arr.toList() as List<WindowManager.LayoutParams>
} else {
params = field[windowManagerObj] as List<WindowManager.LayoutParams>
}
}
} catch (re: RuntimeException) {
return null
} catch (iae: IllegalAccessException) {
return null
}
val roots: ArrayList<RootView> = ArrayList()
views?.let { views ->
params?.let { params ->
for (i in views.indices) {
roots.add(RootView(views[i], params[i]))
}
}
}
return roots as List<RootView>
}
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"
}
}

View File

@@ -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)
}
}