diff --git a/android/sdk/src/main/AndroidManifest.xml b/android/sdk/src/main/AndroidManifest.xml
index 2a1511a39..5ff31b4ab 100644
--- a/android/sdk/src/main/AndroidManifest.xml
+++ b/android/sdk/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
@@ -26,5 +27,11 @@
android:supportsRtl="true">
+
+
+
+
+
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java
index c274fceb5..c00a9666f 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java
@@ -25,6 +25,8 @@ import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.telecom.CallAudioState;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
@@ -100,6 +102,69 @@ class AudioModeModule
*/
static final String TAG = MODULE_NAME;
+ /**
+ * Converts any of the "DEVICE_" constants into the corresponding
+ * {@link CallAudioState} "ROUTE_" number.
+ *
+ * @param audioDevice one of the "DEVICE_" constants.
+ * @return a route number {@link CallAudioState#ROUTE_EARPIECE} if no match
+ * is found.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ private static int audioDeviceToRouteInt(String audioDevice) {
+ if (audioDevice == null) {
+ return CallAudioState.ROUTE_EARPIECE;
+ }
+ switch (audioDevice) {
+ case DEVICE_BLUETOOTH:
+ return CallAudioState.ROUTE_BLUETOOTH;
+ case DEVICE_EARPIECE:
+ return CallAudioState.ROUTE_EARPIECE;
+ case DEVICE_HEADPHONES:
+ return CallAudioState.ROUTE_WIRED_HEADSET;
+ case DEVICE_SPEAKER:
+ return CallAudioState.ROUTE_SPEAKER;
+ default:
+ Log.e(TAG, "Unsupported device name: " + audioDevice);
+ return CallAudioState.ROUTE_EARPIECE;
+ }
+ }
+
+ /**
+ * Populates given route mask into the "DEVICE_" list.
+ *
+ * @param supportedRouteMask an integer coming from
+ * {@link CallAudioState#getSupportedRouteMask()}.
+ * @return a list of device names.
+ */
+ private static Set routesToDeviceNames(int supportedRouteMask) {
+ Set devices = new HashSet<>();
+ if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE)
+ == CallAudioState.ROUTE_EARPIECE) {
+ devices.add(DEVICE_EARPIECE);
+ }
+ if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ devices.add(DEVICE_BLUETOOTH);
+ }
+ if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ devices.add(DEVICE_SPEAKER);
+ }
+ if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ devices.add(DEVICE_HEADPHONES);
+ }
+ return devices;
+ }
+
+ /**
+ * Whether or not the ConnectionService is used for selecting audio devices.
+ */
+ private static boolean useConnectionService() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ }
+
/**
* Indicator that we have lost audio focus.
*/
@@ -204,6 +269,15 @@ class AudioModeModule
*/
private String selectedDevice;
+ /**
+ * Used on API >= 26 to store the most recently reported audio devices.
+ * Makes it easier to compare for a change, because the devices are stored
+ * as a mask in the {@link CallAudioState}. The mask is populated into
+ * the {@link #availableDevices} on each update.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private int supportedRouteMask;
+
/**
* User selected device. When null the default is used depending on the
* mode.
@@ -224,21 +298,25 @@ class AudioModeModule
= (AudioManager)
reactContext.getSystemService(Context.AUDIO_SERVICE);
- // Setup runtime device change detection.
- setupAudioRouteChangeDetection();
+ // Starting Oreo the ConnectionImpl from ConnectionService us used to
+ // detect the available devices.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ // Setup runtime device change detection.
+ setupAudioRouteChangeDetection();
- // Do an initial detection on Android >= M.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- runInAudioThread(onAudioDeviceChangeRunner);
- } else {
- // On Android < M, detect if we have an earpiece.
- PackageManager pm = reactContext.getPackageManager();
- if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
- availableDevices.add(DEVICE_EARPIECE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // Do an initial detection on Android >= M.
+ runInAudioThread(onAudioDeviceChangeRunner);
+ } else {
+ // On Android < M, detect if we have an earpiece.
+ PackageManager pm = reactContext.getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ availableDevices.add(DEVICE_EARPIECE);
+ }
+
+ // Always assume there is a speaker.
+ availableDevices.add(DEVICE_SPEAKER);
}
-
- // Always assume there is a speaker.
- availableDevices.add(DEVICE_SPEAKER);
}
}
@@ -354,6 +432,38 @@ class AudioModeModule
});
}
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ void onCallAudioStateChange(final CallAudioState callAudioState) {
+ runInAudioThread(new Runnable() {
+ @Override
+ public void run() {
+ int newSupportedRoutes = callAudioState.getSupportedRouteMask();
+ boolean audioDevicesChanged
+ = supportedRouteMask != newSupportedRoutes;
+ if (audioDevicesChanged) {
+ supportedRouteMask = newSupportedRoutes;
+ availableDevices = routesToDeviceNames(supportedRouteMask);
+ Log.d(TAG,
+ "Available audio devices: "
+ + availableDevices.toString());
+ }
+
+ boolean audioRouteChanged
+ = audioDeviceToRouteInt(selectedDevice)
+ != callAudioState.getRoute();
+
+ if (audioRouteChanged || audioDevicesChanged) {
+ // Reset user selection
+ userSelectedDevice = null;
+
+ if (mode != -1) {
+ updateAudioRoute(mode);
+ }
+ }
+ }
+ });
+ }
+
/**
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
* when the audio focus of the system is updated.
@@ -417,6 +527,31 @@ class AudioModeModule
});
}
+ /**
+ * The API >= 26 way of adjusting the audio route.
+ *
+ * @param audioDevice one of the "DEVICE_" names to set as the audio route.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private void setAudioRoute(String audioDevice) {
+ int newAudioRoute = audioDeviceToRouteInt(audioDevice);
+
+ RNConnectionService.setAudioRoute(newAudioRoute);
+ }
+
+ /**
+ * The API < 26 way of adjusting the audio route.
+ *
+ * @param audioDevice one of the "DEVICE_" names to set as the audio route.
+ */
+ private void setAudioRoutePreO(String audioDevice) {
+ // Turn bluetooth on / off
+ setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
+
+ // Turn speaker on / off
+ audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
+ }
+
/**
* Helper method to set the output route to a Bluetooth device.
*
@@ -475,7 +610,7 @@ class AudioModeModule
/**
* Setup the audio route change detection mechanism. We use the
- * {@link android.media.AudioDeviceCallback} API on Android >= 23 only.
+ * {@link android.media.AudioDeviceCallback} on 23 >= Android API < 26.
*/
private void setupAudioRouteChangeDetection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -486,7 +621,7 @@ class AudioModeModule
}
/**
- * Audio route change detection mechanism for Android API >= 23.
+ * Audio route change detection mechanism for 23 >= Android API < 26.
*/
@TargetApi(Build.VERSION_CODES.M)
private void setupAudioRouteChangeDetectionM() {
@@ -542,27 +677,31 @@ class AudioModeModule
Log.d(TAG, "Update audio route for mode: " + mode);
if (mode == DEFAULT) {
- audioFocusLost = false;
- audioManager.setMode(AudioManager.MODE_NORMAL);
- audioManager.abandonAudioFocus(this);
- audioManager.setSpeakerphoneOn(false);
- setBluetoothAudioRoute(false);
+ if (!useConnectionService()) {
+ audioFocusLost = false;
+ audioManager.setMode(AudioManager.MODE_NORMAL);
+ audioManager.abandonAudioFocus(this);
+ audioManager.setSpeakerphoneOn(false);
+ setBluetoothAudioRoute(false);
+ }
selectedDevice = null;
userSelectedDevice = null;
return true;
}
- audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
- audioManager.setMicrophoneMute(false);
+ if (!useConnectionService()) {
+ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ audioManager.setMicrophoneMute(false);
- if (audioManager.requestAudioFocus(
+ if (audioManager.requestAudioFocus(
this,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN)
- == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
- Log.d(TAG, "Audio focus request failed");
- return false;
+ == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+ Log.d(TAG, "Audio focus request failed");
+ return false;
+ }
}
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
@@ -596,11 +735,11 @@ class AudioModeModule
selectedDevice = audioDevice;
Log.d(TAG, "Selected audio device: " + audioDevice);
- // Turn bluetooth on / off
- setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
-
- // Turn speaker on / off
- audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
+ if (useConnectionService()) {
+ setAudioRoute(audioDevice);
+ } else {
+ setAudioRoutePreO(audioDevice);
+ }
return true;
}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ConnectionService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ConnectionService.java
new file mode 100644
index 000000000..18af5dd9a
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ConnectionService.java
@@ -0,0 +1,435 @@
+package org.jitsi.meet.sdk;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Jitsi Meet implementation of {@link ConnectionService}. At the time of this
+ * writing it implements only the outgoing call scenario.
+ *
+ * NOTE the class needs to be public, but is not part of the SDK API and should
+ * never be used directly.
+ *
+ * @author Pawel Domas
+ */
+@RequiresApi(api = Build.VERSION_CODES.O)
+public class ConnectionService extends android.telecom.ConnectionService {
+
+ /**
+ * Tag used for logging.
+ */
+ static final String TAG = "JitsiConnectionService";
+
+ /**
+ * The extra added to the {@link ConnectionImpl} and
+ * {@link ConnectionRequest} which stores the {@link PhoneAccountHandle}
+ * created for the call.
+ */
+ static final String EXTRA_PHONE_ACCOUNT_HANDLE
+ = "org.jitsi.meet.sdk.connection_service.PHONE_ACCOUNT_HANDLE";
+
+ /**
+ * Connections mapped by call UUID.
+ */
+ static private final Map connections
+ = new HashMap<>();
+
+ /**
+ * The start call Promises mapped by call UUID.
+ */
+ static private final HashMap startCallPromises
+ = new HashMap<>();
+
+ /**
+ * Adds {@link ConnectionImpl} to the list.
+ *
+ * @param connection - {@link ConnectionImpl}
+ */
+ static void addConnection(ConnectionImpl connection) {
+ connections.put(connection.getCallUUID(), connection);
+ }
+
+ /**
+ * Returns all {@link ConnectionImpl} instances held in this list.
+ *
+ * @return a list of {@link ConnectionImpl}.
+ */
+ static List getConnections() {
+ return new ArrayList<>(connections.values());
+ }
+
+ /**
+ * Registers a start call promise.
+ *
+ * @param uuid - the call UUID to which the start call promise belongs to.
+ * @param promise - the Promise instance to be stored for later use.
+ */
+ static void registerStartCallPromise(String uuid, Promise promise) {
+ startCallPromises.put(uuid, promise);
+ }
+
+ /**
+ * Removes {@link ConnectionImpl} from the list.
+ *
+ * @param connection - {@link ConnectionImpl}
+ */
+ static void removeConnection(ConnectionImpl connection) {
+ connections.remove(connection.getCallUUID());
+ }
+
+ /**
+ * Used to adjusts the connection's state to
+ * {@link android.telecom.Connection#STATE_ACTIVE}.
+ *
+ * @param callUUID the call UUID which identifies the connection.
+ */
+ static void setConnectionActive(String callUUID) {
+ ConnectionImpl connection = connections.get(callUUID);
+
+ if (connection != null) {
+ connection.setActive();
+ } else {
+ Log.e(TAG, String.format(
+ "setConnectionActive - no connection for UUID: %s",
+ callUUID));
+ }
+ }
+
+ /**
+ * Used to adjusts the connection's state to
+ * {@link android.telecom.Connection#STATE_DISCONNECTED}.
+ *
+ * @param callUUID the call UUID which identifies the connection.
+ * @param cause disconnection reason.
+ */
+ static void setConnectionDisconnected(String callUUID, DisconnectCause cause) {
+ ConnectionImpl connection = connections.get(callUUID);
+
+ if (connection != null) {
+ // Note that the connection is not removed from the list here, but
+ // in ConnectionImpl's state changed callback. It's a safer
+ // approach, because in case the app would crash on the JavaScript
+ // side the calls would be cleaned up by the system they would still
+ // be removed from the ConnectionList.
+ connection.setDisconnected(cause);
+ connection.destroy();
+ } else {
+ Log.e(TAG, "endCall no connection for UUID: " + callUUID);
+ }
+ }
+
+ /**
+ * Unregisters a start call promise. Must be called after the Promise is
+ * rejected or resolved.
+ *
+ * @param uuid the call UUID which identifies the call to which the promise
+ * belongs to.
+ * @return the unregistered Promise instance or null if there
+ * wasn't any for the given call UUID.
+ */
+ static Promise unregisterStartCallPromise(String uuid) {
+ return startCallPromises.remove(uuid);
+ }
+
+ /**
+ * Used to adjusts the call's state.
+ *
+ * @param callUUID the call UUID which identifies the connection.
+ * @param callState a map which carries the properties to be modified. See
+ * "KEY_*" constants in {@link ConnectionImpl} for the list of keys.
+ */
+ static void updateCall(String callUUID, ReadableMap callState) {
+ ConnectionImpl connection = connections.get(callUUID);
+
+ if (connection != null) {
+ if (callState.hasKey(ConnectionImpl.KEY_HAS_VIDEO)) {
+ boolean hasVideo
+ = callState.getBoolean(ConnectionImpl.KEY_HAS_VIDEO);
+
+ Log.d(TAG, String.format(
+ "updateCall: %s hasVideo: %s", callUUID, hasVideo));
+ connection.setVideoState(
+ hasVideo
+ ? VideoProfile.STATE_BIDIRECTIONAL
+ : VideoProfile.STATE_AUDIO_ONLY);
+ }
+ } else {
+ Log.e(TAG, "updateCall no connection for UUID: " + callUUID);
+ }
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle accountHandle, ConnectionRequest request) {
+ ConnectionImpl connection = new ConnectionImpl();
+
+ connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
+ connection.setAddress(
+ request.getAddress(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ connection.setExtras(request.getExtras());
+ // NOTE there's a time gap between the placeCall and this callback when
+ // things could get out of sync, but they are put back in sync once
+ // the startCall Promise is resolved below. That's because on
+ // the JavaScript side there's a logic to sync up in .then() callback.
+ connection.setVideoState(request.getVideoState());
+
+ Bundle moreExtras = new Bundle();
+
+ moreExtras.putParcelable(
+ EXTRA_PHONE_ACCOUNT_HANDLE,
+ Objects.requireNonNull(request.getAccountHandle(), "accountHandle"));
+ connection.putExtras(moreExtras);
+
+ addConnection(connection);
+
+ Promise startCallPromise
+ = unregisterStartCallPromise(connection.getCallUUID());
+
+ if (startCallPromise != null) {
+ Log.d(TAG,
+ "onCreateOutgoingConnection " + connection.getCallUUID());
+ startCallPromise.resolve(null);
+ } else {
+ Log.e(TAG, String.format(
+ "onCreateOutgoingConnection: no start call Promise for %s",
+ connection.getCallUUID()));
+ }
+
+ return connection;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle accountHandle, ConnectionRequest request) {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public void onCreateIncomingConnectionFailed(
+ PhoneAccountHandle accountHandle, ConnectionRequest request) {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public void onCreateOutgoingConnectionFailed(
+ PhoneAccountHandle accountHandle, ConnectionRequest request) {
+ PhoneAccountHandle theAccountHandle = request.getAccountHandle();
+ String callUUID = theAccountHandle.getId();
+
+ Log.e(TAG, "onCreateOutgoingConnectionFailed " + callUUID);
+
+ if (callUUID != null) {
+ Promise startCallPromise = unregisterStartCallPromise(callUUID);
+
+ if (startCallPromise != null) {
+ startCallPromise.reject(
+ "CREATE_OUTGOING_CALL_FAILED",
+ "The request has been denied by the system");
+ } else {
+ Log.e(TAG, String.format(
+ "startCallFailed - no start call Promise for UUID: %s",
+ callUUID));
+ }
+ } else {
+ Log.e(TAG, "onCreateOutgoingConnectionFailed - no call UUID");
+ }
+
+ unregisterPhoneAccount(theAccountHandle);
+ }
+
+ private void unregisterPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
+ TelecomManager telecom = getSystemService(TelecomManager.class);
+ if (telecom != null) {
+ if (phoneAccountHandle != null) {
+ telecom.unregisterPhoneAccount(phoneAccountHandle);
+ } else {
+ Log.e(TAG, "unregisterPhoneAccount - account handle is null");
+ }
+ } else {
+ Log.e(TAG, "unregisterPhoneAccount - telecom is null");
+ }
+ }
+
+ /**
+ * Registers new {@link PhoneAccountHandle}.
+ *
+ * @param context the current Android context.
+ * @param address the phone account's address. At the time of this writing
+ * it's the call handle passed from the Java Script side.
+ * @param callUUID the call's UUID for which the account is to be created.
+ * It will be used as the account's id.
+ * @return {@link PhoneAccountHandle} described by the given arguments.
+ */
+ static PhoneAccountHandle registerPhoneAccount(
+ Context context, Uri address, String callUUID) {
+ PhoneAccountHandle phoneAccountHandle
+ = new PhoneAccountHandle(
+ new ComponentName(context, ConnectionService.class),
+ callUUID);
+
+ PhoneAccount.Builder builder
+ = PhoneAccount.builder(phoneAccountHandle, address.toString())
+ .setAddress(address)
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_VIDEO_CALLING |
+ PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_SIP);
+
+ PhoneAccount account = builder.build();
+
+ TelecomManager telecomManager
+ = context.getSystemService(TelecomManager.class);
+ telecomManager.registerPhoneAccount(account);
+
+ return phoneAccountHandle;
+ }
+
+ /**
+ * Connection implementation for Jitsi Meet's {@link ConnectionService}.
+ *
+ * @author Pawel Domas
+ */
+ class ConnectionImpl extends Connection {
+
+ /**
+ * The constant which defines the key for the "has video" property.
+ * The key is used in the map which carries the call's state passed as
+ * the argument of the {@link RNConnectionService#updateCall} method.
+ */
+ static final String KEY_HAS_VIDEO = "hasVideo";
+
+ /**
+ * Called when system wants to disconnect the call.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDisconnect() {
+ Log.d(TAG, "onDisconnect " + getCallUUID());
+ WritableNativeMap data = new WritableNativeMap();
+ data.putString("callUUID", getCallUUID());
+ ReactContextUtils.emitEvent(
+ null,
+ "org.jitsi.meet:features/connection_service#disconnect",
+ data);
+ // The JavaScript side will not go back to the native with
+ // 'endCall', so the Connection must be removed immediately.
+ setConnectionDisconnected(
+ getCallUUID(),
+ new DisconnectCause(DisconnectCause.LOCAL));
+ }
+
+ /**
+ * Called when system wants to abort the call.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAbort() {
+ Log.d(TAG, "onAbort " + getCallUUID());
+ WritableNativeMap data = new WritableNativeMap();
+ data.putString("callUUID", getCallUUID());
+ ReactContextUtils.emitEvent(
+ null,
+ "org.jitsi.meet:features/connection_service#abort",
+ data);
+ // The JavaScript side will not go back to the native with
+ // 'endCall', so the Connection must be removed immediately.
+ setConnectionDisconnected(
+ getCallUUID(),
+ new DisconnectCause(DisconnectCause.CANCELED));
+ }
+
+ @Override
+ public void onHold() {
+ // What ?! Android will still call this method even if we do not add
+ // the HOLD capability, so do the same thing as on abort.
+ // TODO implement HOLD
+ Log.d(TAG, String.format(
+ "onHold %s - HOLD is not supported, aborting the call...",
+ getCallUUID()));
+ this.onAbort();
+ }
+
+ /**
+ * Called when there's change to the call audio state. Either by
+ * the system after the connection is initialized or in response to
+ * {@link #setAudioRoute(int)}.
+ *
+ * @param state the new {@link CallAudioState}
+ */
+ @Override
+ public void onCallAudioStateChanged(CallAudioState state) {
+ Log.d(TAG, "onCallAudioStateChanged: " + state);
+ AudioModeModule audioModeModule
+ = ReactInstanceManagerHolder
+ .getNativeModule(AudioModeModule.class);
+ if (audioModeModule != null) {
+ audioModeModule.onCallAudioStateChange(state);
+ }
+ }
+
+ /**
+ * Unregisters the account when the call is disconnected.
+ *
+ * @param state - the new connection's state.
+ */
+ @Override
+ public void onStateChanged(int state) {
+ Log.d(TAG,
+ String.format("onStateChanged: %s %s",
+ Connection.stateToString(state),
+ getCallUUID()));
+
+ if (state == STATE_DISCONNECTED) {
+ removeConnection(this);
+ unregisterPhoneAccount(getPhoneAccountHandle());
+ }
+ }
+
+ /**
+ * Retrieves the UUID of the call associated with this connection.
+ *
+ * @return call UUID
+ */
+ String getCallUUID() {
+ return getPhoneAccountHandle().getId();
+ }
+
+ private PhoneAccountHandle getPhoneAccountHandle() {
+ return getExtras().getParcelable(
+ ConnectionService.EXTRA_PHONE_ACCOUNT_HANDLE);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ConnectionImpl[adress=%s, uuid=%s]@%d",
+ getAddress(), getCallUUID(), hashCode());
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java
new file mode 100644
index 000000000..15f16a76e
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/RNConnectionService.java
@@ -0,0 +1,165 @@
+package org.jitsi.meet.sdk;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+
+/**
+ * The react-native side of Jitsi Meet's {@link ConnectionService}. Exposes
+ * the Java Script API.
+ *
+ * @author Pawel Domas
+ */
+@RequiresApi(api = Build.VERSION_CODES.O)
+class RNConnectionService
+ extends ReactContextBaseJavaModule {
+
+ private final static String TAG = ConnectionService.TAG;
+
+ /**
+ * Sets the audio route on all existing {@link android.telecom.Connection}s
+ *
+ * @param audioRoute the new audio route to be set. See
+ * {@link android.telecom.CallAudioState} constants prefixed with "ROUTE_".
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ static void setAudioRoute(int audioRoute) {
+ for (ConnectionService.ConnectionImpl c
+ : ConnectionService.getConnections()) {
+ c.setAudioRoute(audioRoute);
+ }
+ }
+
+ RNConnectionService(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ /**
+ * Starts a new outgoing call.
+ *
+ * @param callUUID - unique call identifier assigned by Jitsi Meet to
+ * a conference call.
+ * @param handle - a call handle which by default is Jitsi Meet room's URL.
+ * @param hasVideo - whether or not user starts with the video turned on.
+ * @param promise - the Promise instance passed by the React-native bridge,
+ * so that this method returns a Promise on the JS side.
+ *
+ * NOTE regarding the "missingPermission" suppress - SecurityException will
+ * be handled as part of the Exception try catch block and the Promise will
+ * be rejected.
+ */
+ @SuppressLint("MissingPermission")
+ @ReactMethod
+ public void startCall(
+ String callUUID,
+ String handle,
+ boolean hasVideo,
+ Promise promise) {
+ Log.d(TAG,
+ String.format("startCall UUID=%s, h=%s, v=%s",
+ callUUID,
+ handle,
+ hasVideo));
+
+ ReactApplicationContext ctx = getReactApplicationContext();
+
+ Uri address = Uri.fromParts(PhoneAccount.SCHEME_SIP, handle, null);
+ PhoneAccountHandle accountHandle
+ = ConnectionService.registerPhoneAccount(
+ getReactApplicationContext(), address, callUUID);
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ accountHandle);
+ extras.putInt(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ hasVideo
+ ? VideoProfile.STATE_BIDIRECTIONAL
+ : VideoProfile.STATE_AUDIO_ONLY);
+
+ ConnectionService.registerStartCallPromise(callUUID, promise);
+
+ try {
+ TelecomManager tm
+ = (TelecomManager) ctx.getSystemService(
+ Context.TELECOM_SERVICE);
+
+ tm.placeCall(address, extras);
+ } catch (Exception e) {
+ ConnectionService.unregisterStartCallPromise(callUUID);
+ promise.reject(e);
+ }
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as failed.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @ReactMethod
+ public void reportCallFailed(String callUUID) {
+ Log.d(TAG, "reportCallFailed " + callUUID);
+ ConnectionService.setConnectionDisconnected(
+ callUUID,
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as disconnected.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @ReactMethod
+ public void endCall(String callUUID) {
+ Log.d(TAG, "endCall " + callUUID);
+ ConnectionService.setConnectionDisconnected(
+ callUUID,
+ new DisconnectCause(DisconnectCause.LOCAL));
+ }
+
+ /**
+ * Called by the JS side of things to mark the call as active.
+ *
+ * @param callUUID - the call's UUID.
+ */
+ @ReactMethod
+ public void reportConnectedOutgoingCall(String callUUID) {
+ Log.d(TAG, "reportConnectedOutgoingCall " + callUUID);
+ ConnectionService.setConnectionActive(callUUID);
+ }
+
+ @Override
+ public String getName() {
+ return "ConnectionService";
+ }
+
+ /**
+ * Called by the JS side to update the call's state.
+ *
+ * @param callUUID - the call's UUID.
+ * @param callState - the map which carries infor about the current call's
+ * state. See static fields in {@link ConnectionService.ConnectionImpl}
+ * prefixed with "KEY_" for the values supported by the Android
+ * implementation.
+ */
+ @ReactMethod
+ public void updateCall(String callUUID, ReadableMap callState) {
+ ConnectionService.updateCall(callUUID, callState);
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
index 9fe88a014..6b81641ac 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
@@ -25,11 +25,17 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.common.LifecycleState;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class ReactInstanceManagerHolder {
/**
+ * FIXME (from linter): Do not place Android context classes in static
+ * fields (static reference to ReactInstanceManager which has field
+ * mApplicationContext pointing to Context); this is a memory leak (and
+ * also breaks Instant Run).
+ *
* React Native bridge. The instance manager allows embedding applications
* to create multiple root views off the same JavaScript bundle.
*/
@@ -37,19 +43,26 @@ class ReactInstanceManagerHolder {
private static List createNativeModules(
ReactApplicationContext reactContext) {
- return Arrays.asList(
- new AndroidSettingsModule(reactContext),
- new AppInfoModule(reactContext),
- new AudioModeModule(reactContext),
- new ExternalAPIModule(reactContext),
- new LocaleDetector(reactContext),
- new PictureInPictureModule(reactContext),
- new ProximityModule(reactContext),
- new WiFiStatsModule(reactContext),
- new org.jitsi.meet.sdk.dropbox.Dropbox(reactContext),
- new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
- new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
- );
+ List nativeModules
+ = new ArrayList<>(Arrays.asList(
+ new AndroidSettingsModule(reactContext),
+ new AppInfoModule(reactContext),
+ new AudioModeModule(reactContext),
+ new ExternalAPIModule(reactContext),
+ new LocaleDetector(reactContext),
+ new PictureInPictureModule(reactContext),
+ new ProximityModule(reactContext),
+ new WiFiStatsModule(reactContext),
+ new org.jitsi.meet.sdk.dropbox.Dropbox(reactContext),
+ new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
+ new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)));
+
+ if (android.os.Build.VERSION.SDK_INT
+ >= android.os.Build.VERSION_CODES.O) {
+ nativeModules.add(new RNConnectionService(reactContext));
+ }
+
+ return nativeModules;
}
/**
@@ -58,7 +71,7 @@ class ReactInstanceManagerHolder {
* @param eventName {@code String} containing the event name.
* @param data {@code Object} optional ancillary data for the event.
*/
- public static boolean emitEvent(
+ static boolean emitEvent(
String eventName,
@Nullable Object data) {
ReactInstanceManager reactInstanceManager
diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj
index a10eceba3..19c00f237 100644
--- a/ios/sdk/sdk.xcodeproj/project.pbxproj
+++ b/ios/sdk/sdk.xcodeproj/project.pbxproj
@@ -73,7 +73,7 @@
0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = ""; };
0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = ""; };
- 0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = CallKitIcon.png; path = ../../react/features/mobile/callkit/CallKitIcon.png; sourceTree = ""; };
+ 0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = CallKitIcon.png; path = ../../react/features/mobile/call-integration/CallKitIcon.png; sourceTree = ""; };
0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = ""; };
0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = ""; };
0BCA495E1EC4B6C600B793EE /* Proximity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Proximity.m; sourceTree = ""; };
diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js
index 4e1ecf170..1354627b6 100644
--- a/react/features/app/components/App.native.js
+++ b/react/features/app/components/App.native.js
@@ -15,7 +15,7 @@ import {
import '../../google-api';
import '../../mobile/audio-mode';
import '../../mobile/background';
-import '../../mobile/callkit';
+import '../../mobile/call-integration';
import '../../mobile/external-api';
import '../../mobile/full-screen';
import '../../mobile/permissions';
diff --git a/react/features/mobile/callkit/CallKit.js b/react/features/mobile/call-integration/CallKit.js
similarity index 60%
rename from react/features/mobile/callkit/CallKit.js
rename to react/features/mobile/call-integration/CallKit.js
index ed00c24eb..9b3f76830 100644
--- a/react/features/mobile/callkit/CallKit.js
+++ b/react/features/mobile/call-integration/CallKit.js
@@ -1,5 +1,7 @@
import { NativeModules, NativeEventEmitter } from 'react-native';
+import { getName } from '../../app';
+
/**
* Thin wrapper around Apple's CallKit functionality.
*
@@ -32,7 +34,32 @@ if (CallKit) {
CallKit = {
...CallKit,
- addListener: eventEmitter.addListener.bind(eventEmitter)
+ addListener: eventEmitter.addListener.bind(eventEmitter),
+ registerSubscriptions(context, delegate) {
+ CallKit.setProviderConfiguration({
+ iconTemplateImageName: 'CallKitIcon',
+ localizedName: getName()
+ });
+
+ return [
+ CallKit.addListener(
+ 'performEndCallAction',
+ delegate._onPerformEndCallAction,
+ context),
+ CallKit.addListener(
+ 'performSetMutedCallAction',
+ delegate._onPerformSetMutedCallAction,
+ context),
+
+ // According to CallKit's documentation, when the system resets
+ // we should terminate all calls. Hence, providerDidReset is
+ // the same to us as performEndCallAction.
+ CallKit.addListener(
+ 'providerDidReset',
+ delegate._onPerformEndCallAction,
+ context)
+ ];
+ }
};
}
diff --git a/react/features/mobile/callkit/CallKitIcon.png b/react/features/mobile/call-integration/CallKitIcon.png
similarity index 100%
rename from react/features/mobile/callkit/CallKitIcon.png
rename to react/features/mobile/call-integration/CallKitIcon.png
diff --git a/react/features/mobile/call-integration/ConnectionService.js b/react/features/mobile/call-integration/ConnectionService.js
new file mode 100644
index 000000000..32d82c08e
--- /dev/null
+++ b/react/features/mobile/call-integration/ConnectionService.js
@@ -0,0 +1,33 @@
+import { NativeEventEmitter, NativeModules } from 'react-native';
+
+let ConnectionService = NativeModules.ConnectionService;
+
+// XXX Rather than wrapping ConnectionService in a new class and forwarding
+// the many methods of the latter to the former, add the one additional
+// method that we need to ConnectionService.
+if (ConnectionService) {
+ const eventEmitter = new NativeEventEmitter(ConnectionService);
+
+ ConnectionService = {
+ ...ConnectionService,
+ addListener: eventEmitter.addListener.bind(eventEmitter),
+ registerSubscriptions(context, delegate) {
+ return [
+ ConnectionService.addListener(
+ 'org.jitsi.meet:features/connection_service#disconnect',
+ delegate._onPerformEndCallAction,
+ context),
+ ConnectionService.addListener(
+ 'org.jitsi.meet:features/connection_service#abort',
+ delegate._onPerformEndCallAction,
+ context)
+ ];
+ },
+ setMuted() {
+ // Currently no-op, but remember to remove when implemented on
+ // the native side
+ }
+ };
+}
+
+export default ConnectionService;
diff --git a/react/features/mobile/call-integration/actionTypes.js b/react/features/mobile/call-integration/actionTypes.js
new file mode 100644
index 000000000..c06ebec7b
--- /dev/null
+++ b/react/features/mobile/call-integration/actionTypes.js
@@ -0,0 +1,13 @@
+/**
+ * The type of redux action to set CallKit's and ConnectionService's event
+ * subscriptions.
+ *
+ * {
+ * type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
+ * subscriptions: Array|undefined
+ * }
+ *
+ * @protected
+ */
+export const _SET_CALL_INTEGRATION_SUBSCRIPTIONS
+ = Symbol('_SET_CALL_INTEGRATION_SUBSCRIPTIONS');
diff --git a/react/features/mobile/callkit/index.js b/react/features/mobile/call-integration/index.js
similarity index 100%
rename from react/features/mobile/callkit/index.js
rename to react/features/mobile/call-integration/index.js
diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/call-integration/middleware.js
similarity index 81%
rename from react/features/mobile/callkit/middleware.js
rename to react/features/mobile/call-integration/middleware.js
index 6c1ed9d08..72933a1e3 100644
--- a/react/features/mobile/callkit/middleware.js
+++ b/react/features/mobile/call-integration/middleware.js
@@ -1,9 +1,10 @@
// @flow
+import { Alert } from 'react-native';
import uuid from 'uuid';
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
-import { appNavigate, getName } from '../../app';
+import { appNavigate } from '../../app';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import {
CONFERENCE_FAILED,
@@ -27,8 +28,12 @@ import {
isLocalTrackMuted
} from '../../base/tracks';
-import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
+import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
+
import CallKit from './CallKit';
+import ConnectionService from './ConnectionService';
+
+const CallIntegration = CallKit || ConnectionService;
/**
* Middleware that captures system actions and hooks up CallKit.
@@ -36,9 +41,9 @@ import CallKit from './CallKit';
* @param {Store} store - The redux store.
* @returns {Function}
*/
-CallKit && MiddlewareRegistry.register(store => next => action => {
+CallIntegration && MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
- case _SET_CALLKIT_SUBSCRIPTIONS:
+ case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
return _setCallKitSubscriptions(store, next, action);
case APP_WILL_MOUNT:
@@ -46,7 +51,7 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
case APP_WILL_UNMOUNT:
store.dispatch({
- type: _SET_CALLKIT_SUBSCRIPTIONS,
+ type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
subscriptions: undefined
});
break;
@@ -91,36 +96,21 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
function _appWillMount({ dispatch, getState }, next, action) {
const result = next(action);
- CallKit.setProviderConfiguration({
- iconTemplateImageName: 'CallKitIcon',
- localizedName: getName()
- });
-
const context = {
dispatch,
getState
};
- const subscriptions = [
- CallKit.addListener(
- 'performEndCallAction',
- _onPerformEndCallAction,
- context),
- CallKit.addListener(
- 'performSetMutedCallAction',
- _onPerformSetMutedCallAction,
- context),
- // According to CallKit's documentation, when the system resets we
- // should terminate all calls. Hence, providerDidReset is the same to us
- // as performEndCallAction.
- CallKit.addListener(
- 'providerDidReset',
- _onPerformEndCallAction,
- context)
- ];
+ const delegate = {
+ _onPerformSetMutedCallAction,
+ _onPerformEndCallAction
+ };
- dispatch({
- type: _SET_CALLKIT_SUBSCRIPTIONS,
+ const subscriptions
+ = CallIntegration.registerSubscriptions(context, delegate);
+
+ subscriptions && dispatch({
+ type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
subscriptions
});
@@ -150,7 +140,7 @@ function _conferenceFailed(store, next, action) {
const { callUUID } = action.conference;
if (callUUID) {
- CallKit.reportCallFailed(callUUID);
+ CallIntegration.reportCallFailed(callUUID);
}
}
@@ -176,7 +166,7 @@ function _conferenceJoined(store, next, action) {
const { callUUID } = action.conference;
if (callUUID) {
- CallKit.reportConnectedOutgoingCall(callUUID);
+ CallIntegration.reportConnectedOutgoingCall(callUUID);
}
return result;
@@ -201,7 +191,7 @@ function _conferenceLeft(store, next, action) {
const { callUUID } = action.conference;
if (callUUID) {
- CallKit.endCall(callUUID);
+ CallIntegration.endCall(callUUID);
}
return result;
@@ -220,7 +210,7 @@ function _conferenceLeft(store, next, action) {
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
-function _conferenceWillJoin({ getState }, next, action) {
+function _conferenceWillJoin({ dispatch, getState }, next, action) {
const result = next(action);
const { conference } = action;
@@ -234,7 +224,7 @@ function _conferenceWillJoin({ getState }, next, action) {
// it upper cased.
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
- CallKit.startCall(conference.callUUID, handle, hasVideo)
+ CallIntegration.startCall(conference.callUUID, handle, hasVideo)
.then(() => {
const { callee } = state['features/base/jwt'];
const displayName
@@ -247,9 +237,30 @@ function _conferenceWillJoin({ getState }, next, action) {
state['features/base/tracks'],
MEDIA_TYPE.AUDIO);
- // eslint-disable-next-line object-property-newline
- CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
- CallKit.setMuted(conference.callUUID, muted);
+ CallIntegration.updateCall(
+ conference.callUUID,
+ {
+ displayName,
+ hasVideo
+ });
+ CallIntegration.setMuted(conference.callUUID, muted);
+ })
+ .catch(error => {
+ // Currently this error code is emitted only by Android.
+ if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
+ // We're not tracking the call anymore - it doesn't exist on
+ // the native side.
+ delete conference.callUUID;
+ dispatch(appNavigate(undefined));
+ Alert.alert(
+ 'Call aborted',
+ 'There\'s already another call in progress.'
+ + ' Please end it first and try again.',
+ [
+ { text: 'OK' }
+ ],
+ { cancelable: false });
+ }
});
return result;
@@ -288,7 +299,8 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
if (conference && conference.callUUID === callUUID) {
muted = Boolean(muted); // eslint-disable-line no-param-reassign
- sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
+ sendAnalytics(
+ createTrackMutedEvent('audio', 'call-integration', muted));
dispatch(setAudioMuted(muted, /* ensureTrack */ true));
}
}
@@ -319,7 +331,7 @@ function _setAudioOnly({ getState }, next, action) {
const conference = getCurrentConference(state);
if (conference && conference.callUUID) {
- CallKit.updateCall(
+ CallIntegration.updateCall(
conference.callUUID,
{ hasVideo: !action.audioOnly });
}
@@ -329,20 +341,21 @@ function _setAudioOnly({ getState }, next, action) {
/**
* Notifies the feature callkit that the action
- * {@link _SET_CALLKIT_SUBSCRIPTIONS} is being dispatched within a specific
- * redux {@code store}.
+ * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
+ * a specific redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
- * @param {Action} action - The redux action {@code _SET_CALLKIT_SUBSCRIPTIONS}
- * which is being dispatched in the specified {@code store}.
+ * @param {Action} action - The redux action
+ * {@code _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
+ * the specified {@code store}.
* @private
* @returns {*} The value returned by {@code next(action)}.
*/
function _setCallKitSubscriptions({ getState }, next, action) {
- const { subscriptions } = getState()['features/callkit'];
+ const { subscriptions } = getState()['features/call-integration'];
if (subscriptions) {
for (const subscription of subscriptions) {
@@ -377,11 +390,11 @@ function _syncTrackState({ getState }, next, action) {
const tracks = state['features/base/tracks'];
const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
- CallKit.setMuted(conference.callUUID, muted);
+ CallIntegration.setMuted(conference.callUUID, muted);
break;
}
case 'video': {
- CallKit.updateCall(
+ CallIntegration.updateCall(
conference.callUUID,
{ hasVideo: !isVideoMutedByAudioOnly(state) });
break;
diff --git a/react/features/mobile/callkit/reducer.js b/react/features/mobile/call-integration/reducer.js
similarity index 50%
rename from react/features/mobile/callkit/reducer.js
rename to react/features/mobile/call-integration/reducer.js
index 320685a1f..2983827e1 100644
--- a/react/features/mobile/callkit/reducer.js
+++ b/react/features/mobile/call-integration/reducer.js
@@ -1,13 +1,14 @@
import { assign, ReducerRegistry } from '../../base/redux';
-import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
+import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit';
+import ConnectionService from './ConnectionService';
-CallKit && ReducerRegistry.register(
- 'features/callkit',
+(CallKit || ConnectionService) && ReducerRegistry.register(
+ 'features/call-integration',
(state = {}, action) => {
switch (action.type) {
- case _SET_CALLKIT_SUBSCRIPTIONS:
+ case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
return assign(state, 'subscriptions', action.subscriptions);
}
diff --git a/react/features/mobile/callkit/actionTypes.js b/react/features/mobile/callkit/actionTypes.js
deleted file mode 100644
index d1109d77c..000000000
--- a/react/features/mobile/callkit/actionTypes.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * The type of redux action to set CallKit's event subscriptions.
- *
- * {
- * type: _SET_CALLKIT_SUBSCRIPTIONS,
- * subscriptions: Array|undefined
- * }
- *
- * @protected
- */
-export const _SET_CALLKIT_SUBSCRIPTIONS = Symbol('_SET_CALLKIT_SUBSCRIPTIONS');