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"