diff --git a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperModule.java b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperModule.java index 2c77b0d5b..b18a3fb80 100644 --- a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperModule.java +++ b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperModule.java @@ -7,44 +7,31 @@ package com.facebook.flipper.reactnative; -import com.facebook.flipper.android.AndroidFlipperClient; -import com.facebook.flipper.core.FlipperArray; -import com.facebook.flipper.core.FlipperClient; -import com.facebook.flipper.core.FlipperConnection; -import com.facebook.flipper.core.FlipperObject; -import com.facebook.flipper.core.FlipperPlugin; -import com.facebook.flipper.core.FlipperReceiver; -import com.facebook.flipper.core.FlipperResponder; -import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.module.annotations.ReactModule; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONTokener; +import com.facebook.react.modules.core.DeviceEventManagerModule; +/** + * The FlipperModule is a React Native Native Module. The instance handles incoming calls that + * arrive over the React Native bridge from JavaScript. The instance is created per + * ReactApplicationContext. Which means this module gets reinstated if RN performs a reload. So it + * should not hold any further state on its own. All state is hold by the Plugin and PluginManager + * classes. + */ @ReactModule(name = FlipperModule.NAME) public class FlipperModule extends ReactContextBaseJavaModule { public static final String NAME = "Flipper"; - private final ReactApplicationContext reactContext; - private final FlipperClient flipperClient; - private final Map connections; - private final Map responders; - private final AtomicLong responderId = new AtomicLong(); + private FlipperReactNativeJavaScriptPluginManager manager; - public FlipperModule(ReactApplicationContext reactContext) { + public FlipperModule( + FlipperReactNativeJavaScriptPluginManager manager, ReactApplicationContext reactContext) { super(reactContext); - this.reactContext = reactContext; - this.flipperClient = AndroidFlipperClient.getInstanceIfInitialized(); - this.connections = new ConcurrentHashMap<>(); - this.responders = new ConcurrentHashMap<>(); + this.manager = manager; } @Override @@ -53,118 +40,46 @@ public class FlipperModule extends ReactContextBaseJavaModule { } @ReactMethod - public void registerPlugin( - final String pluginId, - final Boolean inBackground, - final Callback onConnect, - final Callback onDisconnect) { - FlipperPlugin plugin = - new FlipperPlugin() { - @Override - public String getId() { - return pluginId; - } - - @Override - public void onConnect(FlipperConnection connection) throws Exception { - FlipperModule.this.connections.put(pluginId, connection); - onConnect.invoke(); - } - - @Override - public void onDisconnect() throws Exception { - FlipperModule.this.connections.remove(pluginId); - onDisconnect.invoke(); - } - - @Override - public boolean runInBackground() { - return inBackground; - } - }; - this.flipperClient.addPlugin(plugin); + public void registerPlugin(final String pluginId, final Boolean inBackground) { + this.manager.registerPlugin(this, pluginId, inBackground); } @ReactMethod public void send(String pluginId, String method, String data) { - // Optimization: throwing raw strings around to the desktop would probably avoid some double - // parsing... - Object parsedData = FlipperModule.parseJSON(data); - FlipperConnection connection = this.connections.get(pluginId); - if (parsedData instanceof FlipperArray) { - connection.send(method, (FlipperArray) parsedData); - } else { - connection.send(method, (FlipperObject) parsedData); - } + this.manager.send(pluginId, method, data); } @ReactMethod public void reportErrorWithMetadata(String pluginId, String reason, String stackTrace) { - this.connections.get(pluginId).reportErrorWithMetadata(reason, stackTrace); + this.manager.reportErrorWithMetadata(pluginId, reason, stackTrace); } @ReactMethod public void reportError(String pluginId, String error) { - this.connections.get(pluginId).reportError(new Error(error)); + this.manager.reportError(pluginId, error); } @ReactMethod - public void subscribe(String pluginId, String method, final Callback callback) { - this.connections - .get(pluginId) - .receive( - method, - new FlipperReceiver() { - - @Override - public void onReceive(FlipperObject params, FlipperResponder responder) - throws Exception { - String id = String.valueOf(FlipperModule.this.responderId.incrementAndGet()); - FlipperModule.this.responders.put(id, responder); - callback.invoke(params.toJsonString(), id); - } - }); + public void subscribe(String pluginId, String method) { + this.manager.subscribe(this, pluginId, method); } @ReactMethod public void respondSuccess(String responderId, String data) { - FlipperResponder responder = FlipperModule.this.responders.remove(responderId); - if (data == null) { - responder.success(); - } else { - Object parsedData = FlipperModule.parseJSON(data); - if (parsedData instanceof FlipperArray) { - responder.success((FlipperArray) parsedData); - } else { - responder.success((FlipperObject) parsedData); - } - } + this.manager.respondSuccess(responderId, data); } @ReactMethod public void respondError(String responderId, String data) { - FlipperResponder responder = FlipperModule.this.responders.remove(responderId); - Object parsedData = FlipperModule.parseJSON(data); - if (parsedData instanceof FlipperArray) { - responder.success((FlipperArray) parsedData); - } else { - responder.success((FlipperObject) parsedData); - } + this.manager.respondError(responderId, data); } - private static Object /* FlipperArray | FlipperObject */ parseJSON(String json) { - // returns either a FlipperObject or Flipper array, pending the data - try { - JSONTokener tokener = new JSONTokener(json); - if (tokener.nextClean() == '[') { - tokener.back(); - return new FlipperArray(new JSONArray(tokener)); - } else { - tokener.back(); - return new FlipperObject(new JSONObject(tokener)); - } - } catch (JSONException e) { - throw new RuntimeException(e); + public void sendJSEvent(String eventName, WritableMap params) { + ReactApplicationContext context = getReactApplicationContextIfActiveOrWarn(); + if (context != null) { + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); } } } diff --git a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperPackage.java b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperPackage.java index e8000b1b7..ffa8ba16a 100644 --- a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperPackage.java +++ b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperPackage.java @@ -15,10 +15,17 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +/** + * Exposes the react native modules that should be created per ReactApplicationContext. Note that an + * application context lives shorter than the application itself, e.g. reload creates a fresh one. + */ public class FlipperPackage implements ReactPackage { + static FlipperModule flipperModule; + @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new FlipperModule(reactContext)); + return Arrays.asList( + new FlipperModule(FlipperReactNativeJavaScriptPluginManager.getInstance(), reactContext)); } @Override diff --git a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPlugin.java b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPlugin.java new file mode 100644 index 000000000..175adc8a3 --- /dev/null +++ b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPlugin.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.reactnative; + +import com.facebook.flipper.core.FlipperConnection; +import com.facebook.flipper.core.FlipperPlugin; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +/** + * This class holds the state of a single plugin that is created from the JS world trough + * `Flipper.addPlugin`. It's main concern is managing the FlipperConnection to the Desktop client. + * + *

This class is abstract, as Flipper does not support having multiple instances of the same + * class as plugins, But every JS plugin will store it state in a FlipperPlugin, so for every plugin + * we will create an anonymous subclass. + * + *

Note that this class does not directly interact back over the JS bridge to React Native, as + * the JavaPlugin has a longer lifecycle than it's JS counter part, which will be recreated on + * reload. However, if the native module reload, we keep these instances to not loose the connextion + * to the Flipper Desktop client. + */ +public abstract class FlipperReactNativeJavaScriptPlugin implements FlipperPlugin { + String pluginId; + boolean inBackground; + FlipperConnection connection; + FlipperModule module; + + public FlipperReactNativeJavaScriptPlugin( + FlipperModule module, String pluginId, boolean inBackground) { + this.pluginId = pluginId; + this.module = module; + this.inBackground = inBackground; + this.module = module; + } + + @Override + public String getId() { + return pluginId; + } + + @Override + public void onConnect(FlipperConnection connection) throws Exception { + this.connection = connection; + this.fireOnConnect(); + } + + public void fireOnConnect() { + if (!isConnected()) { + throw new RuntimeException("Plugin not connected " + pluginId); + } + this.module.sendJSEvent("react-native-flipper-plugin-connect", getPluginParams()); + } + + @Override + public void onDisconnect() throws Exception { + this.module.sendJSEvent("react-native-flipper-plugin-disconnect", getPluginParams()); + this.connection = null; + } + + @Override + public boolean runInBackground() { + return inBackground; + } + + public boolean isConnected() { + return connection != null; + } + + public FlipperConnection getConnection() { + return connection; + } + + public void setModule(FlipperModule module) { + this.module = module; + } + + WritableMap getPluginParams() { + WritableMap params = Arguments.createMap(); + params.putString("plugin", pluginId); + return params; + } +} diff --git a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPluginManager.java b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPluginManager.java new file mode 100644 index 000000000..84ef078ce --- /dev/null +++ b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptPluginManager.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.reactnative; + +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.core.FlipperArray; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.core.FlipperObject; +import com.facebook.flipper.core.FlipperResponder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * This class manages all loaded FlipperPlugins. It is a singleton to make sure plugin states are + * preserved even when the native modules get reloaded (e.g. due to a reload in RN). This avoids + * loosing our connections with Flipper. + * + *

Note that this manager is not bound to a specific FlipperModule instance, as that might be + * swapped in and out over time. + */ +public class FlipperReactNativeJavaScriptPluginManager { + static FlipperReactNativeJavaScriptPluginManager instance; + + static FlipperReactNativeJavaScriptPluginManager getInstance() { + if (instance == null) { + instance = new FlipperReactNativeJavaScriptPluginManager(); + } + return instance; + } + + private final FlipperClient flipperClient; + + // uniqueResponderId -> ResponderObject + private final Map responders = new ConcurrentHashMap<>(); + // generated the next responder id + private final AtomicLong responderId = new AtomicLong(); + + private FlipperReactNativeJavaScriptPluginManager() { + instance = this; + this.flipperClient = AndroidFlipperClient.getInstanceIfInitialized(); + } + + public void registerPlugin( + FlipperModule module, final String pluginId, final Boolean inBackground) { + FlipperReactNativeJavaScriptPlugin existing = getPlugin(pluginId); + if (existing != null) { + // Make sure events are emitted on the right application context + existing.setModule(module); + // this happens if the plugin hot reloaded on JS side, but we had it here already + if (existing.isConnected()) { + existing.fireOnConnect(); + } + return; + } + // we always create a new plugin class on the fly, + // as Flipper only allows one plugin per type to be registered! + FlipperReactNativeJavaScriptPlugin plugin = + new FlipperReactNativeJavaScriptPlugin(module, pluginId, inBackground) { + // inner class with no new members + }; + this.flipperClient.addPlugin(plugin); + } + + public void send(String pluginId, String method, String data) { + // Optimization: throwing raw strings around to the desktop would probably avoid some double + // parsing... + Object parsedData = parseJSON(data); + FlipperReactNativeJavaScriptPlugin plugin = getPlugin(pluginId); + if (parsedData instanceof FlipperArray) { + plugin.getConnection().send(method, (FlipperArray) parsedData); + } else { + plugin.getConnection().send(method, (FlipperObject) parsedData); + } + } + + public void reportErrorWithMetadata(String pluginId, String reason, String stackTrace) { + getPlugin(pluginId).getConnection().reportErrorWithMetadata(reason, stackTrace); + } + + public void reportError(String pluginId, String error) { + getPlugin(pluginId).getConnection().reportError(new Error(error)); + } + + public void subscribe(FlipperModule module, String pluginId, String method) { + String key = pluginId + "#" + method; + FlipperReactNativeJavaScriptReceiver receiver = + new FlipperReactNativeJavaScriptReceiver(this, module, pluginId, method); + // Fresh connection should be the case for a new subscribe... + getPlugin(pluginId).getConnection().receive(method, receiver); + } + + public void respondSuccess(String responderId, String data) { + FlipperResponder responder = responders.remove(responderId); + if (data == null) { + responder.success(); + } else { + Object parsedData = parseJSON(data); + if (parsedData instanceof FlipperArray) { + responder.success((FlipperArray) parsedData); + } else { + responder.success((FlipperObject) parsedData); + } + } + } + + public void respondError(String responderId, String data) { + FlipperResponder responder = responders.remove(responderId); + Object parsedData = parseJSON(data); + if (parsedData instanceof FlipperArray) { + responder.success((FlipperArray) parsedData); + } else { + responder.success((FlipperObject) parsedData); + } + } + + FlipperReactNativeJavaScriptPlugin getPlugin(String pluginId) { + return this.flipperClient.getPlugin(pluginId); + } + + public String createResponderId(FlipperResponder responder) { + String id = String.valueOf(responderId.incrementAndGet()); + responders.put(id, responder); + return id; + } + + private static Object /* FlipperArray | FlipperObject */ parseJSON(String json) { + // returns either a FlipperObject or Flipper array, pending the data + try { + JSONTokener tokener = new JSONTokener(json); + if (tokener.nextClean() == '[') { + tokener.back(); + return new FlipperArray(new JSONArray(tokener)); + } else { + tokener.back(); + return new FlipperObject(new JSONObject(tokener)); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptReceiver.java b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptReceiver.java new file mode 100644 index 000000000..8c8a99d47 --- /dev/null +++ b/react-native/react-native-flipper/android/src/main/java/com/facebook/flipper/reactnative/FlipperReactNativeJavaScriptReceiver.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.reactnative; + +import com.facebook.flipper.core.FlipperObject; +import com.facebook.flipper.core.FlipperReceiver; +import com.facebook.flipper.core.FlipperResponder; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +public class FlipperReactNativeJavaScriptReceiver implements FlipperReceiver { + String plugin; + String method; + FlipperReactNativeJavaScriptPluginManager manager; + FlipperModule module; + + public FlipperReactNativeJavaScriptReceiver( + FlipperReactNativeJavaScriptPluginManager manager, + FlipperModule module, + String plugin, + String method) { + this.plugin = plugin; + this.method = method; + this.manager = manager; + this.module = module; + } + + @Override + public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception { + String responderId = manager.createResponderId(responder); + WritableMap eventData = Arguments.createMap(); + eventData.putString("plugin", plugin); + eventData.putString("method", method); + eventData.putString("params", params.toJsonString()); + eventData.putString("responderId", responderId); + module.sendJSEvent("react-native-flipper-receive-event", eventData); + } +} diff --git a/react-native/react-native-flipper/index.d.ts b/react-native/react-native-flipper/index.d.ts index b3ea3f8e6..e77195d5b 100644 --- a/react-native/react-native-flipper/index.d.ts +++ b/react-native/react-native-flipper/index.d.ts @@ -57,12 +57,13 @@ declare namespace Flipper { } } +/** + * Internal api to connect to the native Java module, not to be used directly + */ declare module 'Flipper' { export function registerPlugin( pluginId: string, runInBackground: boolean, - onConnect: () => void, - onDisconnect: () => void, ): void; export function send(pluginId: string, method: string, data: string): void; export function reportErrorWithMetadata( @@ -71,13 +72,13 @@ declare module 'Flipper' { stackTrace: string, ): void; export function reportError(pluginId: string, error: string): void; - export function subscribe( - pluginId: string, - method: string, - listener: (data: string, responderId: number) => void, - ): void; + export function subscribe(pluginId: string, method: string): void; export function respondSuccess(responderId: string, data?: string): void; export function respondError(responderId: string, error: string): void; } +/** + * Register a new plugin + * @param plugin + */ export function addPlugin(plugin: Flipper.FlipperPlugin): void; diff --git a/react-native/react-native-flipper/index.js b/react-native/react-native-flipper/index.js index d9b892093..1393eaef5 100644 --- a/react-native/react-native-flipper/index.js +++ b/react-native/react-native-flipper/index.js @@ -8,12 +8,15 @@ */ // $FlowFixMe -import {NativeModules} from 'react-native'; +import {NativeModules, NativeEventEmitter} from 'react-native'; const {Flipper} = NativeModules; export default Flipper; +const listeners = {}; // plugin#method -> callback +const plugins = {}; // plugin -> Plugin + class Connection { connected; pluginId; @@ -42,10 +45,9 @@ class Connection { if (!this.connected) { throw new Error('Cannot receive data, not connected'); } - Flipper.subscribe(this.pluginId, method, (data, responderId) => { - const responder = new Responder(responderId); - listener(JSON.parse(data), responder); - }); + + listeners[this.pluginId + '#' + method] = listener; + Flipper.subscribe(this.pluginId, method); } } @@ -68,6 +70,40 @@ class Responder { } } +function startEventListeners() { + const emitter = new NativeEventEmitter(Flipper); + emitter.removeAllListeners('react-native-flipper-plugin-connect'); + emitter.removeAllListeners('react-native-flipper-plugin-disconnect'); + emitter.removeAllListeners('react-native-flipper-receive-event'); + + emitter.addListener('react-native-flipper-plugin-connect', event => { + const {plugin} = event; + if (plugins[plugin]) { + const p = plugins[plugin]; + p._connection.connected = true; + p.onConnect(p._connection); + } + }); + + emitter.addListener('react-native-flipper-plugin-disconnect', event => { + const {plugin} = event; + if (plugins[plugin]) { + const p = plugins[plugin]; + p._connection.connected = false; + p.onDisconnect(); + } + }); + + emitter.addListener('react-native-flipper-receive-event', event => { + const {plugin, method, params, responderId} = event; + const key = plugin + '#' + method; + if (listeners[key]) { + const responder = new Responder(responderId); + listeners[key](JSON.parse(params), responder); + } + }); +} + // $FlowFixMe export function addPlugin(plugin) { if (!plugin || typeof plugin !== 'object') { @@ -83,18 +119,10 @@ export function addPlugin(plugin) { ? !!plugin.runInBackground() : false; const id = plugin.getId(); - const connection = new Connection(id); + plugin._connection = new Connection(id); + plugins[id] = plugin; - Flipper.registerPlugin( - id, - runInBackground, - function onConnect() { - connection.connected = true; - plugin.onConnect(connection); - }, - function onDisconnect() { - connection.connected = false; - plugin.onDisconnect(); - }, - ); + Flipper.registerPlugin(id, runInBackground); } + +startEventListeners(); diff --git a/src/chrome/mainsidebar/MainSidebar2.tsx b/src/chrome/mainsidebar/MainSidebar2.tsx index ead31249a..53c5a55d2 100644 --- a/src/chrome/mainsidebar/MainSidebar2.tsx +++ b/src/chrome/mainsidebar/MainSidebar2.tsx @@ -71,7 +71,7 @@ const SidebarSectionButton = styled('button')<{ level: SectionLevel; color: string; collapsed: boolean; -}>(({level, color, collapsed}) => ({ +}>(({level, color}) => ({ fontWeight: level === 3 ? 'normal' : 'bold', borderRadius: 0, border: 'none',