android: introduce JitsiMeetConferenceOptions

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
This commit is contained in:
paweldomas 2019-02-08 16:33:07 -06:00 committed by Saúl Ibarra Corretgé
parent aedcfba263
commit 5b3e8a9b5e
5 changed files with 292 additions and 340 deletions

View File

@ -27,21 +27,25 @@ import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetFragment;
import org.jitsi.meet.sdk.JitsiMeetActivityInterface;
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate;
import org.jitsi.meet.sdk.JitsiMeetFragment;
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
import com.crashlytics.android.Crashlytics;
import com.facebook.react.modules.core.PermissionListener;
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
import io.fabric.sdk.android.Fabric;
import java.net.MalformedURLException;
import java.net.URL;
/**
* The one and only {@link Activity} that the Jitsi Meet app needs. The
* The one and only {@link FragmentActivity} that the Jitsi Meet app needs. The
* {@code Activity} is launched in {@code singleTask} mode, so it will be
* created upon application initialization and there will be a single instance
* of it. Further attempts at launching the application once it was already
* launched will result in {@link Activity#onNewIntent(Intent)} being called.
* launched will result in {@link FragmentActivity#onNewIntent(Intent)} being called.
*/
public class MainActivity extends FragmentActivity implements JitsiMeetActivityInterface {
/**
@ -51,14 +55,40 @@ public class MainActivity extends FragmentActivity implements JitsiMeetActivityI
private static final int OVERLAY_PERMISSION_REQUEST_CODE
= (int) (Math.random() * Short.MAX_VALUE);
private static final String TAG = "MainActivity";
private JitsiMeetFragment getFragment() {
return (JitsiMeetFragment) getSupportFragmentManager().findFragmentById(R.id.jitsiFragment);
}
private @Nullable URL buildURL(String urlStr) {
try {
return new URL(urlStr);
} catch (MalformedURLException e) {
return null;
}
}
private void initialize() {
JitsiMeetFragment fragment = getFragment();
fragment.setWelcomePageEnabled(true);
fragment.getJitsiView().join(getIntentUrl(getIntent()));
// Set default options
JitsiMeetConferenceOptions defaultOptions
= new JitsiMeetConferenceOptions.Builder()
.setWelcomePageEnabled(true)
.setServerURL(buildURL("https://meet.jit.si"))
.build();
JitsiMeet.setDefaultConferenceOptions(defaultOptions);
// Join the room specified by the URL the app was launched with.
// Joining without the room option displays the welcome page.
join(getIntentUrl(getIntent()));
}
private void join(@Nullable String url) {
JitsiMeetConferenceOptions options
= new JitsiMeetConferenceOptions.Builder()
.setRoom(url)
.build();
getFragment().getJitsiView().join(options);
}
private @Nullable String getIntentUrl(Intent intent) {
@ -113,7 +143,7 @@ public class MainActivity extends FragmentActivity implements JitsiMeetActivityI
String url;
if ((url = getIntentUrl(intent)) != null) {
getFragment().getJitsiView().join(url);
join(url);
return;
}
@ -145,7 +175,7 @@ public class MainActivity extends FragmentActivity implements JitsiMeetActivityI
}
if (dynamicLink != null) {
getFragment().getJitsiView().join(dynamicLink.toString());
join(dynamicLink.toString());
}
});
}

View File

@ -16,9 +16,29 @@
*/
package org.jitsi.meet.sdk;
import android.os.Bundle;
import com.facebook.react.ReactInstanceManager;
public class JitsiMeet {
private static JitsiMeetConferenceOptions defaultConferenceOptions;
public static JitsiMeetConferenceOptions getDefaultConferenceOptions() {
return defaultConferenceOptions;
}
static Bundle getDefaultProps() {
if (defaultConferenceOptions != null) {
return defaultConferenceOptions.asProps();
}
return new Bundle();
}
public static void setDefaultConferenceOptions(JitsiMeetConferenceOptions options) {
defaultConferenceOptions = options;
}
public static void showDevOptions() {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();

View File

@ -0,0 +1,169 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.os.Bundle;
import java.net.URL;
public class JitsiMeetConferenceOptions {
private URL serverURL;
private String room;
private String token;
private Bundle colorScheme;
private Boolean audioMuted;
private Boolean audioOnly;
private Boolean videoMuted;
private Boolean welcomePageEnabled;
public static class Builder {
private URL serverURL;
private String room;
private String token;
private Bundle colorScheme;
private Boolean audioMuted;
private Boolean audioOnly;
private Boolean videoMuted;
private Boolean welcomePageEnabled;
public Builder() {
}
public Builder setServerURL(URL url) {
this.serverURL = url;
return this;
}
public Builder setRoom(String room) {
this.room = room;
return this;
}
public Builder setToken(String token) {
this.token = token;
return this;
}
public Builder setColorScheme(Bundle colorScheme) {
this.colorScheme = colorScheme;
return this;
}
public Builder setAudioMuted(boolean muted) {
this.audioMuted = muted;
return this;
}
public Builder setAudioOnly(boolean audioOnly) {
this.audioOnly = audioOnly;
return this;
}
public Builder setVideoMuted(boolean videoMuted) {
this.videoMuted = videoMuted;
return this;
}
public Builder setWelcomePageEnabled(boolean enabled) {
this.welcomePageEnabled = enabled;
return this;
}
public JitsiMeetConferenceOptions build() {
JitsiMeetConferenceOptions options = new JitsiMeetConferenceOptions();
options.serverURL = this.serverURL;
options.room = this.room;
options.token = this.token;
options.colorScheme = this.colorScheme;
options.audioMuted = this.audioMuted;
options.audioOnly = this.audioOnly;
options.videoMuted = this.videoMuted;
options.welcomePageEnabled = this.welcomePageEnabled;
return options;
}
}
private JitsiMeetConferenceOptions() {
}
Bundle asProps() {
Bundle props = new Bundle();
if (colorScheme != null) {
props.putBundle("colorScheme", colorScheme);
}
if (welcomePageEnabled != null) {
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
}
// TODO: get rid of this.
props.putBoolean("pictureInPictureEnabled", true);
Bundle config = new Bundle();
if (audioMuted != null) {
config.putBoolean("startWithAudioMuted", audioMuted);
}
if (audioOnly != null) {
config.putBoolean("startAudioOnly", audioOnly);
}
if (videoMuted != null) {
config.putBoolean("startWithVideoMuted", videoMuted);
}
Bundle urlProps = new Bundle();
// The room is fully qualified
if (room != null && room.contains("://")) {
urlProps.putString("url", room);
} else {
if (serverURL != null) {
urlProps.putString("serverURL", serverURL.toString());
}
if (room != null) {
urlProps.putString("room", room);
}
}
if (token != null) {
urlProps.putString("jwt", token);
}
urlProps.putBundle("config", config);
props.putBundle("url", urlProps);
return props;
}
}

View File

@ -41,95 +41,23 @@ import java.net.URL;
*/
public class JitsiMeetFragment extends Fragment {
/**
* A color scheme object to override the default color is the SDK.
*/
private Bundle colorScheme;
/**
* The default base {@code URL} used to join a conference when a partial URL
* (e.g. a room name only) is specified. The value is used only while
* {@link #view} equals {@code null}.
*/
private URL defaultURL;
/**
* Instance of the {@link JitsiMeetView} which this activity will display.
*/
private JitsiMeetView view;
/**
* Whether Picture-in-Picture is enabled. The value is used only while
* {@link #view} equals {@code null}.
*/
private Boolean pictureInPictureEnabled;
/**
* Whether the Welcome page is enabled. The value is used only while
* {@link #view} equals {@code null}.
*/
private boolean welcomePageEnabled;
/**
*
* @see JitsiMeetView#getDefaultURL()
*/
public URL getDefaultURL() {
return view == null ? defaultURL : view.getDefaultURL();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
this.view = initializeView();
return this.view;
}
/**
* Initializes a new {@link JitsiMeetView} instance.
*
* @return a new {@code JitsiMeetView} instance.
*/
protected JitsiMeetView initializeView() {
JitsiMeetView view = new JitsiMeetView(getActivity());
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
// is documented to need such an order in order to take effect:
view.setColorScheme(colorScheme);
view.setDefaultURL(defaultURL);
if (pictureInPictureEnabled != null) {
view.setPictureInPictureEnabled(
pictureInPictureEnabled.booleanValue());
}
view.setWelcomePageEnabled(welcomePageEnabled);
return view;
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return this.view = new JitsiMeetView(getActivity());
}
public JitsiMeetView getJitsiView() {
return view;
}
/**
*
* @see JitsiMeetView#isPictureInPictureEnabled()
*/
public boolean isPictureInPictureEnabled() {
return
view == null
? pictureInPictureEnabled
: view.isPictureInPictureEnabled();
}
/**
*
* @see JitsiMeetView#isWelcomePageEnabled()
*/
public boolean isWelcomePageEnabled() {
return view == null ? welcomePageEnabled : view.isWelcomePageEnabled();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
JitsiMeetActivityDelegate.onActivityResult(
@ -181,53 +109,4 @@ public class JitsiMeetFragment extends Fragment {
view.enterPictureInPicture();
}
}
/**
*
* @see JitsiMeetView#setColorScheme(Bundle)
*/
public void setColorScheme(Bundle colorScheme) {
if (view == null) {
this.colorScheme = colorScheme;
} else {
view.setColorScheme(colorScheme);
}
}
/**
*
* @see JitsiMeetView#setDefaultURL(URL)
*/
public void setDefaultURL(URL defaultURL) {
if (view == null) {
this.defaultURL = defaultURL;
} else {
view.setDefaultURL(defaultURL);
}
}
/**
*
* @see JitsiMeetView#setPictureInPictureEnabled(boolean)
*/
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
if (view == null) {
this.pictureInPictureEnabled
= Boolean.valueOf(pictureInPictureEnabled);
} else {
view.setPictureInPictureEnabled(pictureInPictureEnabled);
}
}
/**
*
* @see JitsiMeetView#setWelcomePageEnabled(boolean)
*/
public void setWelcomePageEnabled(boolean welcomePageEnabled) {
if (view == null) {
this.welcomePageEnabled = welcomePageEnabled;
} else {
view.setWelcomePageEnabled(welcomePageEnabled);
}
}
}

View File

@ -26,11 +26,10 @@ import android.util.Log;
import com.facebook.react.bridge.ReadableMap;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Map;
public class JitsiMeetView
extends BaseReactView<JitsiMeetViewListener> {
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
/**
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
@ -45,25 +44,6 @@ public class JitsiMeetView
*/
private static final String TAG = JitsiMeetView.class.getSimpleName();
/**
* A color scheme object to override the default color is the SDK.
*/
private Bundle colorScheme;
/**
* The default base {@code URL} used to join a conference when a partial URL
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
* {@link #loadURLObject(Bundle)}.
*/
private URL defaultURL;
/**
* Whether Picture-in-Picture is enabled. If {@code null}, defaults to
* {@code true} iff the Android platform supports Picture-in-Picture
* natively.
*/
private Boolean pictureInPictureEnabled;
/**
* The URL of the current conference.
*/
@ -71,10 +51,45 @@ public class JitsiMeetView
// fine to have this field volatile without additional synchronization.
private volatile String url;
/**
* Whether the Welcome page is enabled.
*/
private boolean welcomePageEnabled;
private static Bundle mergeProps(@Nullable Bundle a, @Nullable Bundle b) {
Bundle result = new Bundle();
if (a == null) {
if (b != null) {
result.putAll(b);
}
return result;
}
if (b == null) {
result.putAll(a);
return result;
}
// Start by putting all of a in the result.
result.putAll(a);
// Iterate over each key in b and override if appropriate.
for (String key : b.keySet()) {
Object bValue = b.get(key);
Object aValue = a.get(key);
String valueType = bValue.getClass().getSimpleName();
if (valueType.contentEquals("Boolean")) {
result.putBoolean(key, (Boolean)bValue);
} else if (valueType.contentEquals("String")) {
result.putString(key, (String)bValue);
} else if (valueType.contentEquals("Bundle")) {
result.putBundle(key, mergeProps((Bundle)aValue, (Bundle)bValue));
}
// TODO: handle string arrays when the need arises.
}
return result;
}
public JitsiMeetView(@NonNull Context context) {
super(context);
@ -96,134 +111,31 @@ public class JitsiMeetView
* page.
*/
public void enterPictureInPicture() {
if (isPictureInPictureEnabled() && getURL() != null) {
PictureInPictureModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
PictureInPictureModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
PictureInPictureModule.class);
if (pipModule != null) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
Log.e(TAG, "onUserLeaveHint: failed to enter PiP mode", re);
}
if (pipModule != null
&& PictureInPictureModule.isPictureInPictureSupported()
&& this.url != null) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
Log.e(TAG, "failed to enter PiP mode", re);
}
}
}
/**
* Gets the color scheme used in the SDK.
*
* @return The color scheme map.
*/
public Bundle getColorScheme() {
return colorScheme;
}
/**
* Gets the default base {@code URL} used to join a conference when a
* partial URL (e.g. a room name only) is specified to
* {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. If not
* set or if set to {@code null}, the default built in JavaScript is used:
* https://meet.jit.si
*
* @return The default base {@code URL} or {@code null}.
*/
public URL getDefaultURL() {
return defaultURL;
}
/**
* Gets the URL of the current conference.
*
* XXX The method is meant for internal purposes only at the time of this
* writing because there is no equivalent API on iOS.
*
* @return the URL {@code String} of the current conference if any;
* otherwise, {@code null}.
*/
String getURL() {
return url;
}
/**
* Gets whether Picture-in-Picture is enabled. Picture-in-Picture is
* natively supported on Android API >= 26 (Oreo), so it should not be
* enabled on older platform versions.
*
* @return If Picture-in-Picture is enabled, {@code true}; {@code false},
* otherwise.
*/
public boolean isPictureInPictureEnabled() {
return
PictureInPictureModule.isPictureInPictureSupported()
&& (pictureInPictureEnabled == null
|| pictureInPictureEnabled);
}
/**
* 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 {@code true} if the Welcome page is enabled; otherwise,
* {@code false}.
*/
public boolean isWelcomePageEnabled() {
return welcomePageEnabled;
}
public void join(@Nullable String url) {
Bundle urlObject;
if (url == null) {
urlObject = null;
} else {
urlObject = new Bundle();
urlObject.putString("url", url);
}
loadURL(urlObject);
public void join(@Nullable JitsiMeetConferenceOptions options) {
setProps(options != null ? options.asProps() : new Bundle());
}
public void leave() {
loadURL(null);
setProps(new Bundle());
}
/**
* Loads a specific URL which may identify a conference to join. The URL is
* specified in the form of a {@link Bundle} of properties which (1)
* internally are sufficient to construct a URL {@code String} while (2)
* abstracting the specifics of constructing the URL away from API
* clients/consumers. If the specified URL is {@code null} and the Welcome
* page is enabled, the Welcome page is displayed instead.
*
* @param urlObject The URL to load which may identify a conference to join.
*/
private void loadURL(@Nullable Bundle urlObject) {
Bundle props = new Bundle();
// color scheme
if (colorScheme != null) {
props.putBundle("colorScheme", colorScheme);
}
// defaultURL
if (defaultURL != null) {
props.putString("defaultURL", defaultURL.toString());
}
// pictureInPictureEnabled
props.putBoolean(
"pictureInPictureEnabled",
isPictureInPictureEnabled());
// url
if (urlObject != null) {
props.putBundle("url", urlObject);
}
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
private void setProps(@NonNull Bundle newProps) {
// Merge the default options with the newly provided ones.
Bundle props = mergeProps(JitsiMeet.getDefaultProps(), newProps);
// XXX The method loadURLObject: is supposed to be imperative i.e.
// a second invocation with one and the same URL is expected to join
@ -248,18 +160,18 @@ public class JitsiMeetView
* by/associated with the specified {@code eventName}.
*/
private void maybeSetViewURL(String eventName, ReadableMap eventData) {
String url = eventData.getString("url");
switch(eventName) {
case "CONFERENCE_WILL_JOIN":
setURL(eventData.getString("url"));
this.url = url;
break;
case "CONFERENCE_FAILED":
case "CONFERENCE_WILL_LEAVE":
case "LOAD_CONFIG_ERROR":
String url = eventData.getString("url");
if (url != null && url.equals(getURL())) {
setURL(null);
if (url != null && url.equals(this.url)) {
this.url = null;
}
break;
}
@ -283,62 +195,4 @@ public class JitsiMeetView
onExternalAPIEvent(LISTENER_METHODS, name, data);
}
/**
* Sets the color scheme to override the default colors of the SDK.
*
* @param colorScheme The color scheme map.
*/
public void setColorScheme(Bundle colorScheme) {
this.colorScheme = colorScheme;
}
/**
* Sets the default base {@code URL} used to join a conference when a
* partial URL (e.g. a room name only) is specified to
* {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. Must be
* called before {@link #loadURL(URL)} for it to take effect.
*
* @param defaultURL The {@code URL} to be set as the default base URL.
* @see #getDefaultURL()
*/
public void setDefaultURL(URL defaultURL) {
this.defaultURL = defaultURL;
}
/**
* Sets whether Picture-in-Picture is enabled. Because Picture-in-Picture is
* natively supported only since certain platform versions, specifying
* {@code true} will have no effect on unsupported platform versions.
*
* @param pictureInPictureEnabled To enable Picture-in-Picture,
* {@code true}; otherwise, {@code false}.
*/
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
this.pictureInPictureEnabled = pictureInPictureEnabled;
}
/**
* Sets the URL of the current conference.
*
* XXX The method is meant for internal purposes only. It does not
* {@code loadURL}, it merely remembers the specified URL.
*
* @param url the URL {@code String} which to be set as the URL of the
* current conference.
*/
void setURL(String url) {
this.url = url;
}
/**
* 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;
}
}