diff --git a/android/build.gradle b/android/build.gradle
index 3accf1c6..c4722ded 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -16,11 +16,11 @@ def safeExtGet(prop, fallback) {
}
android {
- compileSdkVersion safeExtGet('compileSdkVersion', 28)
+ compileSdkVersion safeExtGet('compileSdkVersion', 30)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 23)
- targetSdkVersion safeExtGet('targetSdkVersion', 28)
+ targetSdkVersion safeExtGet('targetSdkVersion', 30)
versionCode 1
versionName "1.0"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index abcc0e7d..7b315c5c 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -7,4 +7,8 @@
+
+
+
diff --git a/android/src/main/java/io/wazo/callkeep/Constants.java b/android/src/main/java/io/wazo/callkeep/Constants.java
index 8880a2bd..6b106f32 100644
--- a/android/src/main/java/io/wazo/callkeep/Constants.java
+++ b/android/src/main/java/io/wazo/callkeep/Constants.java
@@ -22,4 +22,6 @@ public class Constants {
public static final String EXTRA_DISABLE_ADD_CALL = "android.telecom.extra.DISABLE_ADD_CALL";
public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128;
+
+ public static final String EXTRA_HAS_VIDEO = "EXTRA_HAS_VIDEO";
}
diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
index 71c47780..43727a2c 100644
--- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
+++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
@@ -43,6 +43,7 @@
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -86,6 +87,8 @@
import static io.wazo.callkeep.Constants.ACTION_CHECK_REACHABILITY;
import static io.wazo.callkeep.Constants.ACTION_WAKE_APP;
import static io.wazo.callkeep.Constants.ACTION_SHOW_INCOMING_CALL_UI;
+import static io.wazo.callkeep.Constants.EXTRA_HAS_VIDEO;
+
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java
public class RNCallKeepModule extends ReactContextBaseJavaModule {
@@ -178,12 +181,12 @@ public void registerEvents() {
}
@ReactMethod
- public void displayIncomingCall(String uuid, String number, String callerName) {
+ public void displayIncomingCall(String uuid, String number, String callerName, Boolean hasVideo) {
if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
return;
}
- Log.d(TAG, "displayIncomingCall, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName);
+ Log.d(TAG, "displayIncomingCall, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName + ", hasVideo: " + hasVideo);
Bundle extras = new Bundle();
Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
@@ -191,6 +194,10 @@ public void displayIncomingCall(String uuid, String number, String callerName) {
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
extras.putString(EXTRA_CALLER_NAME, callerName);
extras.putString(EXTRA_CALL_UUID, uuid);
+ extras.putBoolean(EXTRA_HAS_VIDEO, hasVideo);
+ if (hasVideo) {
+ extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL);
+ }
telecomManager.addNewIncomingCall(handle, extras);
}
@@ -631,7 +638,7 @@ private void registerPhoneAccount(Context appContext) {
builder.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED);
}
else {
- builder.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER);
+ builder.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_VIDEO_CALLING);
}
if (_settings != null && _settings.hasKey("imageName")) {
diff --git a/android/src/main/java/io/wazo/callkeep/VideoConnectionProvider.java b/android/src/main/java/io/wazo/callkeep/VideoConnectionProvider.java
new file mode 100644
index 00000000..7f41fbb9
--- /dev/null
+++ b/android/src/main/java/io/wazo/callkeep/VideoConnectionProvider.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2020 The CallKeep Authors (see the AUTHORS file)
+ * SPDX-License-Identifier: ISC, MIT
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+package io.wazo.callkeep;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.net.Uri;
+import android.telecom.Connection;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import java.lang.IllegalArgumentException;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+import java.util.concurrent.Semaphore;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+
+public class VideoConnectionProvider extends Connection.VideoProvider {
+ private static String TAG = "RNCK:VideoConnectionProvider";
+
+ private Connection mConnection;
+ private CameraCapabilities mCameraCapabilities;
+ private Surface mPreviewSurface;
+ private Surface mRemoteSurface;
+ private Context mContext;
+ private CameraManager mCameraManager;
+ private CameraDevice mCameraDevice;
+ private CameraCaptureSession mCaptureSession;
+ private CaptureRequest mPreviewRequest;
+ private CaptureRequest.Builder mCaptureRequest;
+ private String mCameraId;
+ private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+ private Handler mBackgroundHandler;
+ private HandlerThread mBackgroundThread;
+
+ public VideoConnectionProvider(Context context, Connection connection) {
+ mConnection = connection;
+ mContext = context;
+ mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ public Surface getRemoteSurface() {
+ return mRemoteSurface;
+ }
+
+ @Override
+ public void onSetCamera(String cameraId) {
+ Log.d(TAG, "Set camera to " + cameraId);
+
+ mCameraId = cameraId;
+ setCameraCapabilities(mCameraId);
+ }
+
+ @Override
+ public void onSetPreviewSurface(Surface surface) {
+ Log.d(TAG, "Set preview surface " + (surface == null ? "unset" : "set"));
+
+ mPreviewSurface = surface;
+ if (!TextUtils.isEmpty(mCameraId) && mPreviewSurface != null) {
+ startCamera(mCameraId);
+ }
+ }
+
+ @Override
+ public void onSetDisplaySurface(Surface surface) {
+ Log.d(TAG, "Set display surface " + (surface == null ? "unset" : "set"));
+ mRemoteSurface = surface;
+ }
+
+ @Override
+ public void onSetDeviceOrientation(int rotation) {
+ Log.d(TAG, "Set device orientation " + rotation);
+ }
+
+ /**
+ * Sets the zoom value, creating a new CallCameraCapabalities object. If the zoom value is
+ * non-positive, assume that zoom is not supported.
+ */
+ @Override
+ public void onSetZoom(float value) {
+ Log.d(TAG, "Set zoom to " + value);
+ }
+
+ /**
+ * "Sends" a request with a video call profile. Assumes that this response succeeds and sends
+ * the response back via the CallVideoClient.
+ */
+ @Override
+ public void onSendSessionModifyRequest(final VideoProfile fromProfile, final VideoProfile requestProfile) {
+ Log.d(TAG, "On send session modify request");
+ }
+
+ @Override
+ public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+ Log.d(TAG, "On send session modify response");
+ }
+
+ /**
+ * Returns a CallCameraCapabilities object without supporting zoom.
+ */
+ @Override
+ public void onRequestCameraCapabilities() {
+ Log.d(TAG, "Requested camera capabilities");
+ changeCameraCapabilities(mCameraCapabilities);
+ }
+
+ /**
+ * Randomly reports data usage of value ranging from 10MB to 60MB.
+ */
+ @Override
+ public void onRequestConnectionDataUsage() {
+ Log.d(TAG, "Requested connection data usage");
+ }
+
+ /**
+ * We do not have a need to set a paused image.
+ */
+ @Override
+ public void onSetPauseImage(Uri uri) {
+ Log.d(TAG, "Set pause image");
+ }
+
+ /**
+ * Starts a background thread and its {@link Handler}.
+ */
+ private void startBackgroundThread() {
+ mBackgroundThread = new HandlerThread("CameraBackground");
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+ }
+
+ /**
+ * Stops the background thread and its {@link Handler}.
+ */
+ private void stopBackgroundThread() {
+ mBackgroundThread.quitSafely();
+ try {
+ mBackgroundThread.join();
+ mBackgroundThread = null;
+ mBackgroundHandler = null;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+
+ @Override
+ public void onOpened(CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
+ mCameraDevice = cameraDevice;
+ createCameraPreview();
+ }
+
+ @Override
+ public void onDisconnected(CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ }
+
+ @Override
+ public void onError(CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ }
+
+ };
+
+ /**
+ * Starts displaying the camera image on the preview surface.
+ *
+ * @param cameraId
+ */
+ private void startCamera(String cameraId) {
+ startBackgroundThread();
+
+ try {
+ mCameraManager.openCamera(cameraId, mStateCallback, mBackgroundHandler);
+ } catch (CameraAccessException e) {
+ Log.w(TAG, "CameraAccessException: " + e);
+ return;
+ }
+ }
+
+ private void createCameraPreview() {
+ Log.d(TAG, "Create camera preview");
+
+ if (mPreviewSurface == null) {
+ return;
+ }
+
+ startBackgroundThread();
+
+ try {
+ mCameraDevice.createCaptureSession(Arrays.asList(mPreviewSurface),
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+ if (null == mCameraDevice) {
+ return;
+ }
+
+ mCaptureSession = cameraCaptureSession;
+ try {
+ mCaptureRequest.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+ mPreviewRequest = mCaptureRequest.build();
+ mCaptureSession.setRepeatingRequest(mPreviewRequest,
+ null, null);
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+ }
+
+ }, null
+ );
+
+ mCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ mCaptureRequest.addTarget(mPreviewSurface);
+
+ } catch (CameraAccessException e) {
+ Log.w(TAG, "CameraAccessException: " + e);
+ return;
+ }
+
+ }
+
+ /**
+ * Stops the camera and looper thread.
+ */
+ public void stopCamera() {
+ stopBackgroundThread();
+
+ try {
+ mCameraOpenCloseLock.acquire();
+ if (null != mCaptureSession) {
+ mCaptureSession.close();
+ mCaptureSession = null;
+ }
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+ } finally {
+ mCameraOpenCloseLock.release();
+ }
+ }
+
+ /**
+ * Uses the camera manager to retrieve the camera capabilities for the chosen camera.
+ *
+ * @param cameraId The camera ID to get the capabilities for.
+ */
+ private void setCameraCapabilities(String cameraId) {
+ Log.d(TAG, "Set camera capabilities");
+ if (cameraId == null) {
+ return;
+ }
+
+ CameraManager cameraManager = (CameraManager) mContext.getSystemService(
+ Context.CAMERA_SERVICE);
+ CameraCharacteristics c = null;
+ try {
+ c = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (IllegalArgumentException | CameraAccessException e) {
+ // Ignoring camera problems.
+ }
+ if (c != null) {
+ // Get the video size for the camera
+ StreamConfigurationMap map = c.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ Size previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
+ mCameraCapabilities = new CameraCapabilities(previewSize.getWidth(),
+ previewSize.getHeight());
+ }
+ }
+}
diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java
index 75d5f55f..63a3b597 100644
--- a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java
+++ b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java
@@ -93,10 +93,12 @@ public void onCallAudioStateChanged(CallAudioState state) {
}
@Override
- public void onAnswer() {
- super.onAnswer();
+ public void onAnswer(int videoState) {
+ super.onAnswer(videoState);
Log.d(TAG, "onAnswer called");
+ setVideoState(videoState);
+
setConnectionCapabilities(getConnectionCapabilities() | Connection.CAPABILITY_HOLD);
setAudioModeIsVoip(true);
@@ -223,4 +225,5 @@ public void run() {
}
});
}
+
}
diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
index f1087e33..87b88fdd 100644
--- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
+++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
@@ -43,6 +43,7 @@
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
import android.util.Log;
import com.facebook.react.HeadlessJsTaskService;
@@ -67,6 +68,7 @@
import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID;
import static io.wazo.callkeep.Constants.EXTRA_DISABLE_ADD_CALL;
import static io.wazo.callkeep.Constants.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static io.wazo.callkeep.Constants.EXTRA_HAS_VIDEO;
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionService.java
@TargetApi(Build.VERSION_CODES.M)
@@ -93,7 +95,6 @@ public static Connection getConnection(String connectionId) {
public VoiceConnectionService() {
super();
- Log.e(TAG, "Constructor");
currentConnectionRequest = null;
currentConnectionService = this;
}
@@ -150,7 +151,13 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage
Log.d(TAG, "onCreateIncomingConnection, name:" + name);
+ Boolean hasVideo = extra.getBoolean(EXTRA_HAS_VIDEO, false);
Connection incomingCallConnection = createConnection(request);
+
+ if (hasVideo) {
+ setVideoCallSupport(this.getApplicationContext(), incomingCallConnection);
+ }
+
incomingCallConnection.setRinging();
incomingCallConnection.setInitialized();
@@ -346,7 +353,6 @@ private Connection createConnection(ConnectionRequest request) {
}
VoiceConnection connection = new VoiceConnection(this, extrasMap);
- connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Context context = getApplicationContext();
@@ -363,6 +369,15 @@ private Connection createConnection(ConnectionRequest request) {
}
}
+ int capabilities = connection.getConnectionCapabilities();
+ capabilities |= Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
+ capabilities |= Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
+ capabilities |= Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO;
+ capabilities |= Connection.CAPABILITY_MUTE;
+ capabilities |= Connection.CAPABILITY_SUPPORT_HOLD;
+ capabilities |= Connection.CAPABILITY_HOLD;
+ connection.setConnectionCapabilities(capabilities);
+
connection.setInitializing();
connection.setExtras(extras);
currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection);
@@ -420,6 +435,13 @@ public void run() {
});
}
+ private void setVideoCallSupport(Context context, Connection connection) {
+ VideoConnectionProvider VideoCallProvider = new VideoConnectionProvider(context, connection);
+
+ connection.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ connection.setVideoProvider(VideoCallProvider);
+ }
+
private HashMap bundleToMap(Bundle extras) {
HashMap extrasMap = new HashMap<>();
Set keySet = extras.keySet();
diff --git a/index.js b/index.js
index 13362ac4..0c19d90b 100644
--- a/index.js
+++ b/index.js
@@ -79,7 +79,7 @@ class RNCallKeep {
options = null
) => {
if (!isIOS) {
- RNCallKeepModule.displayIncomingCall(uuid, handle, localizedCallerName);
+ RNCallKeepModule.displayIncomingCall(uuid, handle, localizedCallerName, hasVideo);
return;
}
diff --git a/package.json b/package.json
index 1a3fbfda..6ca2445a 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,12 @@
"url": "git+https://github.com/react-native-webrtc/react-native-callkeep.git"
},
"author": "See AUTHORS file",
- "maintainers": ["See AUTHORS file"],
- "contributors": ["See AUTHORS file"],
+ "maintainers": [
+ "See AUTHORS file"
+ ],
+ "contributors": [
+ "See AUTHORS file"
+ ],
"license": "ISC",
"bugs": {
"url": "https://github.com/react-native-webrtc/react-native-callkeep/issues"