mirror of https://github.com/jitsi/jitsi-meet
As the need for adding more views connected with our React code arises, having everything in JitsiMeetView is not going to scale. In order to pave the way for multiple apps / views feeding off the React side, the following changes have been made: - All base functionality related to creating a ReactRootView and layout are now in BaseReactView - All Activity lifecycle methods that need to be called by any activity holding a BaseReactView are now conveniently placed in ReactActivityLifecycleAdapter - ExternalAPIModule has been refactored to cater for multiple views: events are delivered to views, and its their resposibility to deal with them - Following on the previous point, ListenerUtils is a utility class for helping with the translation from events into listener methodspull/3260/head
parent
cd1c384cc8
commit
9972e88b67
@ -0,0 +1,171 @@ |
||||
package org.jitsi.meet.sdk; |
||||
|
||||
import android.app.Activity; |
||||
import android.content.Context; |
||||
import android.os.Bundle; |
||||
import android.support.annotation.NonNull; |
||||
import android.support.annotation.Nullable; |
||||
import android.util.Log; |
||||
import android.widget.FrameLayout; |
||||
|
||||
import com.facebook.react.ReactRootView; |
||||
import com.facebook.react.bridge.ReadableMap; |
||||
import com.rnimmersive.RNImmersiveModule; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Set; |
||||
import java.util.UUID; |
||||
import java.util.WeakHashMap; |
||||
|
||||
/** |
||||
* Base class for all views which are backed by a React Native view. |
||||
*/ |
||||
public abstract class BaseReactView extends FrameLayout { |
||||
/** |
||||
* Background color used by {@code BaseReactView} and the React Native root |
||||
* view. |
||||
*/ |
||||
protected static int BACKGROUND_COLOR = 0xFF111111; |
||||
|
||||
/** |
||||
* The unique identifier of this {@code BaseReactView} within the process |
||||
* for the purposes of {@link ExternalAPIModule}. The name scope was |
||||
* inspired by postis which we use on Web for the similar purposes of the |
||||
* iframe-based external API. |
||||
*/ |
||||
protected final String externalAPIScope; |
||||
|
||||
/** |
||||
* React Native root view. |
||||
*/ |
||||
private ReactRootView reactRootView; |
||||
|
||||
/** |
||||
* Collection with all created views. This is used for finding the right |
||||
* view when delivering events coming from the {@link ExternalAPIModule}; |
||||
*/ |
||||
static final Set<BaseReactView> views |
||||
= Collections.newSetFromMap(new WeakHashMap<BaseReactView, Boolean>()); |
||||
|
||||
/** |
||||
* Find a view which matches the given external API scope. |
||||
* |
||||
* @param externalAPIScope - Scope for the view we want to find. |
||||
* @return The found {@code BaseReactView}, or {@code null}. |
||||
*/ |
||||
public static BaseReactView findViewByExternalAPIScope( |
||||
String externalAPIScope) { |
||||
synchronized (views) { |
||||
for (BaseReactView view : views) { |
||||
if (view.externalAPIScope.equals(externalAPIScope)) { |
||||
return view; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
public BaseReactView(@NonNull Context context) { |
||||
super(context); |
||||
|
||||
setBackgroundColor(BACKGROUND_COLOR); |
||||
|
||||
ReactInstanceManagerHolder.initReactInstanceManager( |
||||
((Activity) context).getApplication()); |
||||
|
||||
// Hook this BaseReactView into ExternalAPI.
|
||||
externalAPIScope = UUID.randomUUID().toString(); |
||||
synchronized (views) { |
||||
views.add(this); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates the {@code ReactRootView} for the given app name with the given |
||||
* props. Once created it's set as the view of this {@code FrameLayout}. |
||||
* |
||||
* @param appName - Name of the "app" (in React Native terms) which we want |
||||
* to load. |
||||
* @param props - Props (in React terms) to be passed to the app. |
||||
*/ |
||||
public void createReactRootView(String appName, @Nullable Bundle props) { |
||||
if (props == null) { |
||||
props = new Bundle(); |
||||
} |
||||
|
||||
// Set externalAPIScope
|
||||
props.putString("externalAPIScope", externalAPIScope); |
||||
|
||||
if (reactRootView == null) { |
||||
reactRootView = new ReactRootView(getContext()); |
||||
reactRootView.startReactApplication( |
||||
ReactInstanceManagerHolder.getReactInstanceManager(), |
||||
appName, |
||||
props); |
||||
reactRootView.setBackgroundColor(BACKGROUND_COLOR); |
||||
addView(reactRootView); |
||||
} else { |
||||
reactRootView.setAppProperties(props); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Releases the React resources (specifically the {@link ReactRootView}) |
||||
* associated with this view. |
||||
* |
||||
* This method MUST be called when the Activity holding this view is |
||||
* destroyed, typically in the {@code onDestroy} method. |
||||
*/ |
||||
public void dispose() { |
||||
if (reactRootView != null) { |
||||
removeView(reactRootView); |
||||
reactRootView.unmountReactApplication(); |
||||
reactRootView = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Abstract method called by {@link ExternalAPIModule} when an event is |
||||
* received for this view. |
||||
* |
||||
* @param name - Name of the event. |
||||
* @param data - Event data. |
||||
*/ |
||||
public abstract void onExternalAPIEvent(String name, ReadableMap data); |
||||
|
||||
/** |
||||
* Called when the window containing this view gains or loses focus. |
||||
* |
||||
* @param hasFocus If the window of this view now has focus, {@code true}; |
||||
* otherwise, {@code false}. |
||||
*/ |
||||
@Override |
||||
public void onWindowFocusChanged(boolean hasFocus) { |
||||
super.onWindowFocusChanged(hasFocus); |
||||
|
||||
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
|
||||
|
||||
// FIXME The singleton pattern employed by RNImmersiveModule is not
|
||||
// advisable because a react-native mobule is consumable only after its
|
||||
// BaseJavaModule#initialize() has completed and here we have no
|
||||
// knowledge of whether the precondition is really met.
|
||||
RNImmersiveModule immersive = RNImmersiveModule.getInstance(); |
||||
|
||||
if (hasFocus && immersive != null) { |
||||
try { |
||||
immersive.emitImmersiveStateChangeEvent(); |
||||
} catch (RuntimeException re) { |
||||
// FIXME I don't know how to check myself whether
|
||||
// BaseJavaModule#initialize() has been invoked and thus
|
||||
// RNImmersiveModule is consumable. A safe workaround is to
|
||||
// swallow the failure because the whole full-screen/immersive
|
||||
// functionality is brittle anyway, akin to the icing on the
|
||||
// cake, and has been working without onWindowFocusChanged for a
|
||||
// very long time.
|
||||
Log.e("RNImmersiveModule", |
||||
"emitImmersiveStateChangeEvent() failed!", re); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,150 @@ |
||||
package org.jitsi.meet.sdk; |
||||
|
||||
import com.facebook.react.bridge.ReadableMap; |
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator; |
||||
import com.facebook.react.bridge.UiThreadUtil; |
||||
|
||||
import java.lang.reflect.InvocationTargetException; |
||||
import java.lang.reflect.Method; |
||||
import java.lang.reflect.Modifier; |
||||
import java.util.HashMap; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
* Utility methods for helping with transforming {@link ExternalAPIModule} |
||||
* events into listener methods. Used with descendants of {@link BaseReactView}. |
||||
*/ |
||||
public final class ListenerUtils { |
||||
/** |
||||
* Extracts the methods defined in a listener and creates a mapping of this |
||||
* form: event name -> method. |
||||
* |
||||
* @param listener - The listener whose methods we want to slurp. |
||||
* @return A mapping with event names - methods. |
||||
*/ |
||||
public static Map<String, Method> slurpListenerMethods(Class listener) { |
||||
final Map<String, Method> methods = new HashMap<>(); |
||||
|
||||
// Figure out the mapping between the listener methods
|
||||
// and the events i.e. redux action types.
|
||||
Pattern onPattern = Pattern.compile("^on[A-Z]+"); |
||||
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)"); |
||||
|
||||
for (Method method : listener.getDeclaredMethods()) { |
||||
// * The method must be public (because it is declared by an
|
||||
// interface).
|
||||
// * The method must be/return void.
|
||||
if (!Modifier.isPublic(method.getModifiers()) |
||||
|| !Void.TYPE.equals(method.getReturnType())) { |
||||
continue; |
||||
} |
||||
|
||||
// * The method name must start with "on" followed by a
|
||||
// capital/uppercase letter (in agreement with the camelcase
|
||||
// coding style customary to Java in general and the projects of
|
||||
// the Jitsi community in particular).
|
||||
String name = method.getName(); |
||||
|
||||
if (!onPattern.matcher(name).find()) { |
||||
continue; |
||||
} |
||||
|
||||
// * The method must accept/have exactly 1 parameter of a type
|
||||
// assignable from HashMap.
|
||||
Class<?>[] parameterTypes = method.getParameterTypes(); |
||||
|
||||
if (parameterTypes.length != 1 |
||||
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) { |
||||
continue; |
||||
} |
||||
|
||||
// Convert the method name to an event name.
|
||||
name |
||||
= camelcasePattern.matcher(name.substring(2)) |
||||
.replaceAll("$1_$2") |
||||
.toUpperCase(Locale.ROOT); |
||||
methods.put(name, method); |
||||
} |
||||
|
||||
return methods; |
||||
} |
||||
|
||||
/** |
||||
* Executes the right listener method for the given event. |
||||
* NOTE: This function will run asynchronously on the UI thread. |
||||
* |
||||
* @param listener - The listener on which the method will be called. |
||||
* @param listenerMethods - Mapping with event names and the matching |
||||
* methods. |
||||
* @param eventName - Name of the event. |
||||
* @param eventData - Data associated with the event. |
||||
*/ |
||||
public static void runListenerMethod( |
||||
final Object listener, |
||||
final Map<String, Method> listenerMethods, |
||||
final String eventName, |
||||
final ReadableMap eventData) { |
||||
// Make sure listener methods are invoked on the UI thread. It
|
||||
// was requested by SDK consumers.
|
||||
if (UiThreadUtil.isOnUiThread()) { |
||||
runListenerMethodOnUiThread( |
||||
listener, listenerMethods, eventName, eventData); |
||||
} else { |
||||
UiThreadUtil.runOnUiThread(new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
runListenerMethodOnUiThread( |
||||
listener, listenerMethods, eventName, eventData); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Helper companion for {@link ListenerUtils#runListenerMethod} which runs |
||||
* in the UI thread. |
||||
*/ |
||||
private static void runListenerMethodOnUiThread( |
||||
Object listener, |
||||
Map<String, Method> listenerMethods, |
||||
String eventName, |
||||
ReadableMap eventData) { |
||||
UiThreadUtil.assertOnUiThread(); |
||||
|
||||
Method method = listenerMethods.get(eventName); |
||||
if (method != null) { |
||||
try { |
||||
method.invoke(listener, toHashMap(eventData)); |
||||
} catch (IllegalAccessException e) { |
||||
throw new RuntimeException(e); |
||||
} catch (InvocationTargetException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Initializes a new {@code HashMap} instance with the key-value |
||||
* associations of a specific {@code ReadableMap}. |
||||
* |
||||
* @param readableMap the {@code ReadableMap} specifying the key-value |
||||
* associations with which the new {@code HashMap} instance is to be |
||||
* initialized. |
||||
* @return a new {@code HashMap} instance initialized with the key-value |
||||
* associations of the specified {@code readableMap}. |
||||
*/ |
||||
private static HashMap<String, Object> toHashMap(ReadableMap readableMap) { |
||||
HashMap<String, Object> hashMap = new HashMap<>(); |
||||
|
||||
for (ReadableMapKeySetIterator i = readableMap.keySetIterator(); |
||||
i.hasNextKey();) { |
||||
String key = i.nextKey(); |
||||
|
||||
hashMap.put(key, readableMap.getString(key)); |
||||
} |
||||
|
||||
return hashMap; |
||||
} |
||||
} |
@ -0,0 +1,113 @@ |
||||
package org.jitsi.meet.sdk; |
||||
|
||||
import android.app.Activity; |
||||
import android.content.Intent; |
||||
|
||||
import com.facebook.react.ReactInstanceManager; |
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; |
||||
|
||||
/** |
||||
* Helper class to encapsulate the work which needs to be done on Activity |
||||
* lifecycle methods in order for the React side to be aware of it. |
||||
*/ |
||||
class ReactActivityLifecycleAdapter { |
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onBackPressed} so we can do the required internal |
||||
* processing. |
||||
* |
||||
* @return {@code true} if the back-press was processed; {@code false}, |
||||
* otherwise. If {@code false}, the application should call the parent's |
||||
* implementation. |
||||
*/ |
||||
public static boolean onBackPressed() { |
||||
ReactInstanceManager reactInstanceManager |
||||
= ReactInstanceManagerHolder.getReactInstanceManager(); |
||||
|
||||
if (reactInstanceManager == null) { |
||||
return false; |
||||
} else { |
||||
reactInstanceManager.onBackPressed(); |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onDestroy} so we can do the required internal |
||||
* processing. |
||||
* |
||||
* @param activity {@code Activity} being destroyed. |
||||
*/ |
||||
public static void onHostDestroy(Activity activity) { |
||||
ReactInstanceManager reactInstanceManager |
||||
= ReactInstanceManagerHolder.getReactInstanceManager(); |
||||
|
||||
if (reactInstanceManager != null) { |
||||
reactInstanceManager.onHostDestroy(activity); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onPause} so we can do the required internal processing. |
||||
* |
||||
* @param activity {@code Activity} being paused. |
||||
*/ |
||||
public static void onHostPause(Activity activity) { |
||||
ReactInstanceManager reactInstanceManager |
||||
= ReactInstanceManagerHolder.getReactInstanceManager(); |
||||
|
||||
if (reactInstanceManager != null) { |
||||
reactInstanceManager.onHostPause(activity); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onResume} so we can do the required internal processing. |
||||
* |
||||
* @param activity {@code Activity} being resumed. |
||||
*/ |
||||
public static void onHostResume(Activity activity) { |
||||
onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity)); |
||||
} |
||||
|
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onResume} so we can do the required internal processing. |
||||
* |
||||
* @param activity {@code Activity} being resumed. |
||||
* @param defaultBackButtonImpl a {@code DefaultHardwareBackBtnHandler} to |
||||
* handle invoking the back button if no {@code JitsiMeetView} handles it. |
||||
*/ |
||||
public static void onHostResume( |
||||
Activity activity, |
||||
DefaultHardwareBackBtnHandler defaultBackButtonImpl) { |
||||
ReactInstanceManager reactInstanceManager |
||||
= ReactInstanceManagerHolder.getReactInstanceManager(); |
||||
|
||||
if (reactInstanceManager != null) { |
||||
reactInstanceManager.onHostResume(activity, defaultBackButtonImpl); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Activity lifecycle method which should be called from |
||||
* {@code Activity.onNewIntent} so we can do the required internal |
||||
* processing. Note that this is only needed if the activity's "launchMode" |
||||
* was set to "singleTask". This is required for deep linking to work once |
||||
* the application is already running. |
||||
* |
||||
* @param intent {@code Intent} instance which was received. |
||||
*/ |
||||
public static void onNewIntent(Intent intent) { |
||||
ReactInstanceManager reactInstanceManager |
||||
= ReactInstanceManagerHolder.getReactInstanceManager(); |
||||
|
||||
if (reactInstanceManager != null) { |
||||
reactInstanceManager.onNewIntent(intent); |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue