[Android] Allow multiple JitsiMeetViews

This commit is contained in:
Lyubo Marinov 2017-06-10 03:34:11 -05:00
parent 10e5e0fdf5
commit 4b2add7aa6
4 changed files with 166 additions and 157 deletions

View File

@ -30,6 +30,10 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState; import com.facebook.react.common.LifecycleState;
import java.net.URL; import java.net.URL;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
public class JitsiMeetView extends FrameLayout { public class JitsiMeetView extends FrameLayout {
/** /**
@ -38,87 +42,24 @@ public class JitsiMeetView extends FrameLayout {
*/ */
private static final int BACKGROUND_COLOR = 0xFF111111; private static final int BACKGROUND_COLOR = 0xFF111111;
/**
* Reference to the single instance of this class we currently allow. It's
* currently used for fetching the instance from the listener's callbacks.
*
* TODO: Lift this limitation.
*/
private static JitsiMeetView instance;
/** /**
* React Native bridge. The instance manager allows embedding applications * React Native bridge. The instance manager allows embedding applications
* to create multiple root views off the same JavaScript bundle. * to create multiple root views off the same JavaScript bundle.
*/ */
private static ReactInstanceManager reactInstanceManager; private static ReactInstanceManager reactInstanceManager;
/** private static final Set<JitsiMeetView> views
* {@link JitsiMeetViewListener} instance for reporting events occurring in = Collections.newSetFromMap(new WeakHashMap<JitsiMeetView, Boolean>());
* Jitsi Meet.
*/
private JitsiMeetViewListener listener;
/** public static JitsiMeetView findViewByExternalAPIScope(
* React Native root view. String externalAPIScope) {
*/ for (JitsiMeetView view : views) {
private ReactRootView reactRootView; if (view.externalAPIScope.equals(externalAPIScope)) {
return view;
/** }
* Whether the Welcome page is enabled.
*/
private boolean welcomePageEnabled;
public JitsiMeetView(@NonNull Context context) {
super(context);
if (instance != null) {
throw new RuntimeException(
"Only a single instance is currently allowed");
} }
/* return null;
* TODO: Only allow a single instance for now. All React Native modules
* are kinda singletons so global state would be broken since we have a
* single bridge. Once we have that sorted out multiple instances of
* JitsiMeetView will be allowed.
*/
instance = this;
setBackgroundColor(BACKGROUND_COLOR);
if (reactInstanceManager == null) {
initReactInstanceManager(((Activity) context).getApplication());
}
}
/**
* Returns the only instance of this class we currently allow creating.
*
* @return The {@code JitsiMeetView} instance.
*/
public static JitsiMeetView getInstance() {
return instance;
}
/**
* Gets the {@link JitsiMeetViewListener} set on this {@code JitsiMeetView}.
*
* @return The {@code JitsiMeetViewListener} set on this
* {@code JitsiMeetView}.
*/
public JitsiMeetViewListener getListener() {
return listener;
}
/**
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
* page is rendered when this {@code JitsiMeetView} is not at a URL
* identifying a Jitsi Meet conference/room.
*
* @return {@true} if the Welcome page is enabled; otherwise, {@code false}.
*/
public boolean getWelcomePageEnabled() {
return welcomePageEnabled;
} }
/** /**
@ -130,74 +71,24 @@ public class JitsiMeetView extends FrameLayout {
* @param application - <tt>Application</tt> instance which is running. * @param application - <tt>Application</tt> instance which is running.
*/ */
private static void initReactInstanceManager(Application application) { private static void initReactInstanceManager(Application application) {
reactInstanceManager = ReactInstanceManager.builder() reactInstanceManager
.setApplication(application) = ReactInstanceManager.builder()
.setBundleAssetName("index.android.bundle") .setApplication(application)
.setJSMainModuleName("index.android") .setBundleAssetName("index.android.bundle")
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage()) .setJSMainModuleName("index.android")
.addPackage(new com.facebook.react.shell.MainReactPackage()) .addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.oblador.vectoricons.VectorIconsPackage()) .addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.ocetnik.timer.BackgroundTimerPackage()) .addPackage(new com.oblador.vectoricons.VectorIconsPackage())
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage()) .addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
.addPackage(new com.rnimmersive.RNImmersivePackage()) .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
.addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage()) .addPackage(new com.rnimmersive.RNImmersivePackage())
.addPackage(new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage()) .addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage())
.addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage()) .addPackage(
.setUseDeveloperSupport(BuildConfig.DEBUG) new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage())
.setInitialLifecycleState(LifecycleState.RESUMED) .addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage())
.build(); .setUseDeveloperSupport(BuildConfig.DEBUG)
} .setInitialLifecycleState(LifecycleState.RESUMED)
.build();
/**
* Loads the given URL and displays the conference. If the specified URL is
* null, the welcome page is displayed instead.
*
* @param url - The conference URL.
*/
public void loadURL(@Nullable URL url) {
Bundle props = new Bundle();
// url
if (url != null) {
props.putString("url", url.toString());
}
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
// TODO: ReactRootView#setAppProperties is only available on React
// Native 0.45, so destroy the current root view and create a new one.
if (reactRootView != null) {
removeView(reactRootView);
reactRootView = null;
}
reactRootView = new ReactRootView(getContext());
reactRootView
.startReactApplication(reactInstanceManager, "App", props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
}
/**
* Sets a specific {@link JitsiMeetViewListener} on this
* {@code JitsiMeetView}.
*
* @param listener - The {@code JitsiMeetViewListener} to set on this
* {@code JitsiMeetView}.
*/
public void setListener(JitsiMeetViewListener listener) {
this.listener = listener;
}
/**
* Sets whether the Welcome page is enabled. Must be called before
* {@link #loadURL(URL)} for it to take effect.
*
* @param welcomePageEnabled {@code true} to enable the Welcome page;
* otherwise, {@code false}.
*/
public void setWelcomePageEnabled(boolean welcomePageEnabled) {
this.welcomePageEnabled = welcomePageEnabled;
} }
/** /**
@ -269,4 +160,117 @@ public class JitsiMeetView extends FrameLayout {
reactInstanceManager.onNewIntent(intent); reactInstanceManager.onNewIntent(intent);
} }
} }
/**
* The unique identifier of this {@code JitsiMeetView} within the process
* for the purposes of {@link ExternalAPI}. The name scope was inspired by
* postis which we use on Web for the similar purposes of the iframe-based
* external API.
*/
private final String externalAPIScope;
/**
* {@link JitsiMeetViewListener} instance for reporting events occurring in
* Jitsi Meet.
*/
private JitsiMeetViewListener listener;
/**
* React Native root view.
*/
private ReactRootView reactRootView;
/**
* Whether the Welcome page is enabled.
*/
private boolean welcomePageEnabled;
public JitsiMeetView(@NonNull Context context) {
super(context);
setBackgroundColor(BACKGROUND_COLOR);
if (reactInstanceManager == null) {
initReactInstanceManager(((Activity) context).getApplication());
}
// Hook this JitsiMeetView into ExternalAPI.
externalAPIScope = UUID.randomUUID().toString();
views.add(this);
}
/**
* Gets the {@link JitsiMeetViewListener} set on this {@code JitsiMeetView}.
*
* @return The {@code JitsiMeetViewListener} set on this
* {@code JitsiMeetView}.
*/
public JitsiMeetViewListener getListener() {
return listener;
}
/**
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
* page is rendered when this {@code JitsiMeetView} is not at a URL
* identifying a Jitsi Meet conference/room.
*
* @return {@true} if the Welcome page is enabled; otherwise, {@code false}.
*/
public boolean getWelcomePageEnabled() {
return welcomePageEnabled;
}
/**
* Loads the given URL and displays the conference. If the specified URL is
* null, the welcome page is displayed instead.
*
* @param url - The conference URL.
*/
public void loadURL(@Nullable URL url) {
Bundle props = new Bundle();
// externalAPIScope
props.putString("externalAPIScope", externalAPIScope);
// url
if (url != null) {
props.putString("url", url.toString());
}
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
// TODO: ReactRootView#setAppProperties is only available on React
// Native 0.45, so destroy the current root view and create a new one.
if (reactRootView != null) {
removeView(reactRootView);
reactRootView = null;
}
reactRootView = new ReactRootView(getContext());
reactRootView
.startReactApplication(reactInstanceManager, "App", props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
}
/**
* Sets a specific {@link JitsiMeetViewListener} on this
* {@code JitsiMeetView}.
*
* @param listener - The {@code JitsiMeetViewListener} to set on this
* {@code JitsiMeetView}.
*/
public void setListener(JitsiMeetViewListener listener) {
this.listener = listener;
}
/**
* Sets whether the Welcome page is enabled. Must be called before
* {@link #loadURL(URL)} for it to take effect.
*
* @param welcomePageEnabled {@code true} to enable the Welcome page;
* otherwise, {@code false}.
*/
public void setWelcomePageEnabled(boolean welcomePageEnabled) {
this.welcomePageEnabled = welcomePageEnabled;
}
} }

View File

@ -62,22 +62,31 @@ public class ExternalAPIModule extends ReactContextBaseJavaModule {
/** /**
* Dispatches an event that occurred on JavaScript to the view's listener. * Dispatches an event that occurred on JavaScript to the view's listener.
* *
* @param name - Event name. * @param name The name of the event.
* @param data - Ancillary data for the event. * @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
* @param scope
*/ */
@ReactMethod @ReactMethod
public void sendEvent(final String name, ReadableMap data) { public void sendEvent(String name, ReadableMap data, String scope) {
JitsiMeetView view = JitsiMeetView.getInstance(); // The JavaScript App needs to provide uniquely identifying information
JitsiMeetViewListener listener // to the native ExternalAPI module so that the latter may match the
= view != null ? view.getListener() : null; // former to the native JitsiMeetView which hosts it.
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
if (view == null) {
return;
}
JitsiMeetViewListener listener = view.getListener();
if (listener == null) { if (listener == null) {
return; return;
} }
// TODO: Sigh, converting a ReadableMap to a HashMap is not supported // TODO Converting a ReadableMap to a HashMap is not supported until
// until React Native 0.46. // React Native 0.46.
final HashMap<String, Object> dataMap = new HashMap<>(); HashMap<String, Object> dataMap = new HashMap<>();
switch (name) { switch (name) {
case "CONFERENCE_FAILED": case "CONFERENCE_FAILED":

View File

@ -29,8 +29,10 @@ RCT_EXPORT_MODULE();
/** /**
* Dispatches an event that occurred on JavaScript to the view's delegate. * Dispatches an event that occurred on JavaScript to the view's delegate.
* *
* - name: name of the event. * @param name The name of the event.
* - data: dictionary (JSON object in JS) with data associated with the event. * @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
* @param scope
*/ */
RCT_EXPORT_METHOD(sendEvent:(NSString *)name RCT_EXPORT_METHOD(sendEvent:(NSString *)name
data:(NSDictionary *)data data:(NSDictionary *)data

View File

@ -2,7 +2,6 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import { Platform } from '../../base/react';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
@ -102,13 +101,8 @@ function _sendEvent(store: Object, name: string, data: Object) {
if (app) { if (app) {
const { externalAPIScope } = app.props; const { externalAPIScope } = app.props;
// TODO Lift the restriction on the JitsiMeetView instance count on
// Android as well.
if (externalAPIScope) { if (externalAPIScope) {
NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope); NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
} else if (Platform.OS === 'android') {
NativeModules.ExternalAPI.sendEvent(name, data);
console.warn(name);
} }
} }
} }