Initial commit for incoming call screen.
This commit is contained in:
parent
3f3a957f40
commit
45c2a657af
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* 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 com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
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;
|
||||
|
||||
abstract class AbstractExternalAPIModule<T> extends ReactContextBaseJavaModule {
|
||||
|
||||
private static Map<String, Method> createAPIMethodMap(Class<?> listenerClass) {
|
||||
Map<String, Method> result = new HashMap<>();
|
||||
// Figure out the mapping between the JitsiMeetViewListener 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 : listenerClass.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);
|
||||
result.put(name, method);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private final Map<String, Method> methodMap;
|
||||
|
||||
/**
|
||||
* Initializes a new module instance. There shall be a single instance of
|
||||
* this module throughout the lifetime of the application.
|
||||
*
|
||||
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||
* is created.
|
||||
*/
|
||||
AbstractExternalAPIModule(ReactApplicationContext reactContext,
|
||||
Class<T> listenerClass) {
|
||||
super(reactContext);
|
||||
this.methodMap = createAPIMethodMap(listenerClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||
* the specified {@code View}'s listener on the UI thread.
|
||||
*
|
||||
* @param name The name of the event.
|
||||
* @param data The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code name}.
|
||||
* @param scope
|
||||
*/
|
||||
@ReactMethod
|
||||
public void sendEvent(
|
||||
final String name,
|
||||
final ReadableMap data,
|
||||
final String scope) {
|
||||
// Make sure listener is invoked on the UI thread. It was requested by
|
||||
// SDK consumers.
|
||||
if (!UiThreadUtil.isOnUiThread()) {
|
||||
sendEventOnUiThread(name, data, scope);
|
||||
} else {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendEventOnUiThread(name, data, scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEventOnUiThread(final String name,
|
||||
final ReadableMap data,
|
||||
final String scope) {
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native AbstractExternalAPI module so that the latter may match
|
||||
// the former to the native View which hosts it.
|
||||
T listener = findListenerByExternalAPIScope(scope);
|
||||
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Method method = methodMap.get(name);
|
||||
|
||||
if (method != null) {
|
||||
try {
|
||||
method.invoke(listener, toHashMap(data));
|
||||
} catch (IllegalAccessException e) {
|
||||
// FIXME There was a multicatch for IllegalAccessException and
|
||||
// InvocationTargetException, but Android Studio complained
|
||||
// with: "Multi-catch with these reflection exceptions requires
|
||||
// API level 19 (current min is 16) because they get compiled to
|
||||
// the common but new super type ReflectiveOperationException.
|
||||
// As a workaround either create individual catch statements, or
|
||||
// catch Exception."
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract T findListenerByExternalAPIScope(String scope);
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
}
|
|
@ -1,35 +1,8 @@
|
|||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* 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 com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Module implementing a simple API to enable a proximity sensor-controlled
|
||||
|
@ -37,66 +10,16 @@ import java.util.regex.Pattern;
|
|||
* object it will dim the screen and disable touch controls. The functionality
|
||||
* is used with the conference audio-only mode.
|
||||
*/
|
||||
class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||
/**
|
||||
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
|
||||
* redux action types.
|
||||
*/
|
||||
private static final Map<String, Method> JITSI_MEET_VIEW_LISTENER_METHODS
|
||||
= new HashMap<>();
|
||||
|
||||
static {
|
||||
// Figure out the mapping between the JitsiMeetViewListener 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 : JitsiMeetViewListener.class.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);
|
||||
JITSI_MEET_VIEW_LISTENER_METHODS.put(name, method);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalAPIModule extends AbstractExternalAPIModule<JitsiMeetViewListener> {
|
||||
/**
|
||||
* Initializes a new module instance. There shall be a single instance of
|
||||
* this module throughout the lifetime of the application.
|
||||
*
|
||||
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||
* is created.
|
||||
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||
* is created.
|
||||
*/
|
||||
public ExternalAPIModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
super(reactContext, JitsiMeetViewListener.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,6 +62,16 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JitsiMeetViewListener findListenerByExternalAPIScope(String scope) {
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native ExternalAPI module so that the latter may match the
|
||||
// former to the native JitsiMeetView which hosts it.
|
||||
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
||||
|
||||
return view != null ? view.getListener() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||
* the specified {@link JitsiMeetView}'s listener.
|
||||
|
@ -148,107 +81,15 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
|||
* by/associated with the specified {@code name}.
|
||||
* @param scope
|
||||
*/
|
||||
@Override
|
||||
@ReactMethod
|
||||
public void sendEvent(final String name,
|
||||
final ReadableMap data,
|
||||
final String scope) {
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native ExternalAPI module so that the latter may match the
|
||||
// former to the native JitsiMeetView which hosts it.
|
||||
public void sendEvent(String name, ReadableMap data, String scope) {
|
||||
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
||||
|
||||
if (view == null) {
|
||||
return;
|
||||
if (view != null) {
|
||||
maybeSetViewURL(name, data, view);
|
||||
}
|
||||
|
||||
// XXX The JitsiMeetView property URL was introduced in order to address
|
||||
// an exception in the Picture-in-Picture functionality which arose
|
||||
// because of delays related to bridging between JavaScript and Java. To
|
||||
// reduce these delays do not wait for the call to be transfered to the
|
||||
// UI thread.
|
||||
maybeSetViewURL(name, data, view);
|
||||
|
||||
// Make sure JitsiMeetView's listener is invoked on the UI thread. It
|
||||
// was requested by SDK consumers.
|
||||
if (UiThreadUtil.isOnUiThread()) {
|
||||
sendEventOnUiThread(name, data, scope);
|
||||
} else {
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendEventOnUiThread(name, data, scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||
* the specified {@link JitsiMeetView}'s listener on the UI thread.
|
||||
*
|
||||
* @param name The name of the event.
|
||||
* @param data The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code name}.
|
||||
* @param scope
|
||||
*/
|
||||
private void sendEventOnUiThread(final String name,
|
||||
final ReadableMap data,
|
||||
final String scope) {
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native ExternalAPI module so that the latter may match the
|
||||
// former to the native JitsiMeetView which hosts it.
|
||||
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
||||
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetViewListener listener = view.getListener();
|
||||
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name);
|
||||
|
||||
if (method != null) {
|
||||
try {
|
||||
method.invoke(listener, toHashMap(data));
|
||||
} catch (IllegalAccessException e) {
|
||||
// FIXME There was a multicatch for IllegalAccessException and
|
||||
// InvocationTargetException, but Android Studio complained
|
||||
// with: "Multi-catch with these reflection exceptions requires
|
||||
// API level 19 (current min is 16) because they get compiled to
|
||||
// the common but new super type ReflectiveOperationException.
|
||||
// As a workaround either create individual catch statements, or
|
||||
// catch Exception."
|
||||
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 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;
|
||||
super.sendEvent(name, data, scope);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package org.jitsi.meet.sdk;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
class IncomingCallExternalAPIModule extends AbstractExternalAPIModule<IncomingCallViewListener> {
|
||||
|
||||
/**
|
||||
* Initializes a new module instance. There shall be a single instance of
|
||||
* this module throughout the lifetime of the application.
|
||||
*
|
||||
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||
* is created.
|
||||
*/
|
||||
public IncomingCallExternalAPIModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext, IncomingCallViewListener.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of this module to be used in the React Native bridge.
|
||||
*
|
||||
* @return The name of this module to be used in the React Native bridge.
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return "IncomingCallExternalAPI";
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingCallViewListener findListenerByExternalAPIScope(String scope) {
|
||||
IncomingCallView view = IncomingCallView.findViewByExternalAPIScope(scope);
|
||||
|
||||
return view != null ? view.getListener() : null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.facebook.react.ReactRootView;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
public class IncomingCallView extends FrameLayout {
|
||||
|
||||
private static final int BACKGROUND_COLOR = 0xFF111111;
|
||||
|
||||
private static final Set<IncomingCallView> views
|
||||
= Collections.newSetFromMap(new WeakHashMap<IncomingCallView, Boolean>());
|
||||
|
||||
public static IncomingCallView findViewByExternalAPIScope(
|
||||
String externalAPIScope) {
|
||||
synchronized (views) {
|
||||
for (IncomingCallView view : views) {
|
||||
if (view.externalAPIScope.equals(externalAPIScope)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final class IncomingCallInfo {
|
||||
private final String callerName;
|
||||
private final String callerAvatarUrl;
|
||||
|
||||
public IncomingCallInfo(@NonNull String callerName, @NonNull String callerAvatarUrl) {
|
||||
this.callerName = callerName;
|
||||
this.callerAvatarUrl = callerAvatarUrl;
|
||||
}
|
||||
|
||||
public String getCallerName() {
|
||||
return callerName;
|
||||
}
|
||||
|
||||
public String getCallerAvatarUrl() {
|
||||
return callerAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private final String externalAPIScope;
|
||||
private IncomingCallViewListener listener;
|
||||
private ReactRootView reactRootView;
|
||||
|
||||
public IncomingCallView(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
if (ReactInstanceManagerHolder.getReactInstanceManager() == null) {
|
||||
ReactInstanceManagerHolder.initReactInstanceManager(((Activity) context).getApplication());
|
||||
}
|
||||
setBackgroundColor(BACKGROUND_COLOR);
|
||||
externalAPIScope = UUID.randomUUID().toString();
|
||||
synchronized (views) {
|
||||
views.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (reactRootView != null) {
|
||||
removeView(reactRootView);
|
||||
reactRootView.unmountReactApplication();
|
||||
reactRootView = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IncomingCallViewListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public void setListener(IncomingCallViewListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void loadIncomingCallInfo(IncomingCallInfo callInfo) {
|
||||
Bundle props = new Bundle();
|
||||
|
||||
props.putString("externalAPIScope", externalAPIScope);
|
||||
props.putString("url", "");
|
||||
props.putString("callerName", callInfo.getCallerName());
|
||||
props.putString("callerAvatarUrl", callInfo.getCallerAvatarUrl());
|
||||
if (reactRootView == null) {
|
||||
reactRootView = new ReactRootView(getContext());
|
||||
reactRootView.startReactApplication(
|
||||
ReactInstanceManagerHolder.getReactInstanceManager(),
|
||||
"IncomingCallApp",
|
||||
props);
|
||||
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
|
||||
addView(reactRootView);
|
||||
} else {
|
||||
reactRootView.setAppProperties(props);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.jitsi.meet.sdk;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IncomingCallViewListener {
|
||||
void onIncomingCallAnswered(Map<String, Object> data);
|
||||
|
||||
void onIncomingCallDeclined(Map<String, Object> data);
|
||||
}
|
|
@ -42,6 +42,7 @@ public class ReactInstanceManagerHolder {
|
|||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new IncomingCallExternalAPIModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext),
|
||||
|
|
|
@ -643,5 +643,10 @@
|
|||
"rejected": "Rejected",
|
||||
"ignored": "Ignored",
|
||||
"expired": "Expired"
|
||||
},
|
||||
"incomingCall": {
|
||||
"title": "Incoming call",
|
||||
"answer": "Answer",
|
||||
"decline": "Decline"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* The type of redux action to receive an incoming call.
|
||||
*
|
||||
* {
|
||||
* type: INCOMING_CALL_RECEIVED,
|
||||
* caller: Object
|
||||
* }
|
||||
*/
|
||||
export const INCOMING_CALL_RECEIVED = Symbol('INCOMING_CALL_RECEIVED');
|
||||
|
||||
/**
|
||||
* The type of redux action to answer an incoming call.
|
||||
*
|
||||
* {
|
||||
* type: INCOMING_CALL_ANSWERED,
|
||||
* }
|
||||
*/
|
||||
export const INCOMING_CALL_ANSWERED = Symbol('INCOMING_CALL_ANSWERED');
|
||||
|
||||
/**
|
||||
* The type of redux action to decline an incoming call.
|
||||
*
|
||||
* {
|
||||
* type: INCOMING_CALL_DECLINED,
|
||||
* }
|
||||
*/
|
||||
export const INCOMING_CALL_DECLINED = Symbol('INCOMING_CALL_DECLINED');
|
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
INCOMING_CALL_RECEIVED,
|
||||
INCOMING_CALL_ANSWERED,
|
||||
INCOMING_CALL_DECLINED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Shows a received incoming call.
|
||||
*
|
||||
* @param {Object} caller - The caller of an incoming call.
|
||||
* @returns {{
|
||||
* type: INCOMING_CALL_RECEIVED,
|
||||
* caller: Object
|
||||
* }}
|
||||
*/
|
||||
export function incomingCallReceived(caller: Object) {
|
||||
return {
|
||||
type: INCOMING_CALL_RECEIVED,
|
||||
caller
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a received incoming call.
|
||||
*
|
||||
* @returns {{
|
||||
* type: INCOMING_CALL_ANSWERED
|
||||
* }}
|
||||
*/
|
||||
export function incomingCallAnswered() {
|
||||
return {
|
||||
type: INCOMING_CALL_ANSWERED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Declines a received incoming call.
|
||||
*
|
||||
* @returns {{
|
||||
* type: INCOMING_CALL_DECLINED
|
||||
* }}
|
||||
*/
|
||||
export function incomingCallDeclined() {
|
||||
return {
|
||||
type: INCOMING_CALL_DECLINED
|
||||
};
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { App } from '../../../app';
|
||||
import { RouteRegistry } from '../../../base/react';
|
||||
import { incomingCallReceived } from '../actions';
|
||||
|
||||
import IncomingCallPage from './IncomingCallPage';
|
||||
|
||||
/**
|
||||
* Root application component for incoming call.
|
||||
*
|
||||
* @extends App
|
||||
*/
|
||||
export default class IncomingCallApp extends App {
|
||||
|
||||
/**
|
||||
* Creates incoming call when component is going to be mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
super.componentWillMount();
|
||||
this._init.then(() => {
|
||||
const { dispatch } = this._getStore();
|
||||
|
||||
dispatch(incomingCallReceived({
|
||||
name: this.props.callerName,
|
||||
avatarUrl: this.props.callerAvatarUrl
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to {@code IncomingCallPage}.
|
||||
*
|
||||
* @param {Object|string} url - Ingored.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_openURL(url) { // eslint-disable-line no-unused-vars
|
||||
this._navigate(RouteRegistry.getRouteByComponent(IncomingCallPage));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import {
|
||||
incomingCallAnswered,
|
||||
incomingCallDeclined
|
||||
} from '../actions';
|
||||
|
||||
type Props = {
|
||||
_callerName: string,
|
||||
_callerAvatarUrl: string,
|
||||
_onAnswered: Function,
|
||||
_onDeclined: Function,
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The React {@code Component} displays an incoming call screen.
|
||||
*/
|
||||
class IncomingCallPage extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return ( // TODO: layout and styles
|
||||
<View>
|
||||
<Text>{ this.props._callerName }</Text>
|
||||
<Text onPress = { this.props._onAnswered }>
|
||||
{ t('incomingCall.answer') }
|
||||
</Text>
|
||||
<Text onPress = { this.props._onDeclined }>
|
||||
{ t('incomingCall.decline') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some action to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _onAnswered: Function,
|
||||
* _onDeclined: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches an action to answer an incoming call.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAnswered() {
|
||||
dispatch(incomingCallAnswered());
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches an action to decline an incoming call.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDeclined() {
|
||||
dispatch(incomingCallDeclined());
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the component's props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _callerName: string,
|
||||
* _callerAvatarUrl: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { caller } = state['features/mobile/incoming-call'] || {};
|
||||
|
||||
return (caller && {
|
||||
/**
|
||||
* The caller's name.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
_callerName: caller.name,
|
||||
|
||||
/**
|
||||
* The caller's avatar url.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
_callerAvatarUrl: caller.avatarUrl
|
||||
|
||||
}) || {};
|
||||
}
|
||||
|
||||
export default translate(
|
||||
connect(_mapStateToProps, _mapDispatchToProps)(IncomingCallPage));
|
|
@ -0,0 +1,2 @@
|
|||
export { default as IncomingCallPage } from './IncomingCallPage';
|
||||
export { default as IncomingCallApp } from './IncomingCallApp';
|
|
@ -0,0 +1,14 @@
|
|||
import {
|
||||
BoxModel,
|
||||
ColorPalette,
|
||||
createStyleSheet
|
||||
} from '../../../base/styles';
|
||||
|
||||
const TEXT_COLOR = ColorPalette.white;
|
||||
|
||||
export default createStyleSheet({
|
||||
button: {
|
||||
color: TEXT_COLOR,
|
||||
margin: BoxModel.margin
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './route';
|
||||
import './reducer';
|
|
@ -0,0 +1,58 @@
|
|||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
import {
|
||||
INCOMING_CALL_ANSWERED,
|
||||
INCOMING_CALL_DECLINED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Middleware that captures Redux actions and uses the IncomingCallExternalAPI
|
||||
* module to turn them into native events so the application knows about them.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case INCOMING_CALL_ANSWERED:
|
||||
_sendEvent(store, 'INCOMING_CALL_ANSWERED', /* data */ {});
|
||||
break;
|
||||
case INCOMING_CALL_DECLINED:
|
||||
_sendEvent(store, 'INCOMING_CALL_DECLINED', /* data */ {});
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sends a specific event to the native counterpart of the External API. Native
|
||||
* apps may listen to such events via the mechanisms provided by the (native)
|
||||
* mobile Jitsi Meet SDK.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {string} name - The name of the event to send.
|
||||
* @param {Object} data - The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code name}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendEvent(
|
||||
{ getState }: { getState: Function },
|
||||
name: string,
|
||||
data: Object) {
|
||||
const { app } = getState()['features/app'];
|
||||
|
||||
if (app) {
|
||||
const { externalAPIScope } = app.props;
|
||||
|
||||
if (externalAPIScope) {
|
||||
NativeModules
|
||||
.IncomingCallExternalAPI.sendEvent(name, data, externalAPIScope);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import { assign, ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import { INCOMING_CALL_RECEIVED } from './actionTypes';
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/mobile/incoming-call',
|
||||
(state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case INCOMING_CALL_RECEIVED:
|
||||
return assign(state, {
|
||||
caller: action.caller
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/* @flow */
|
||||
|
||||
import { RouteRegistry } from '../../base/react';
|
||||
|
||||
import { IncomingCallPage } from './components';
|
||||
|
||||
/**
|
||||
* Register route for {@code IncomingCallPage}.
|
||||
*/
|
||||
RouteRegistry.register({
|
||||
component: IncomingCallPage,
|
||||
path: '/:incoming-call'
|
||||
});
|
|
@ -18,6 +18,7 @@ import { AppRegistry, Linking, NativeModules } from 'react-native';
|
|||
|
||||
import { App } from './features/app';
|
||||
import { equals } from './features/base/redux';
|
||||
import { IncomingCallApp } from './features/mobile/incoming-call';
|
||||
|
||||
/**
|
||||
* React Native doesn't support specifying props to the main/root component (in
|
||||
|
@ -159,3 +160,6 @@ class Root extends Component {
|
|||
|
||||
// Register the main/root Component.
|
||||
AppRegistry.registerComponent('App', () => Root);
|
||||
|
||||
// Register the incoming call Component.
|
||||
AppRegistry.registerComponent('IncomingCallApp', () => IncomingCallApp);
|
||||
|
|
Loading…
Reference in New Issue