Skip to content

[Android] Video support on incomingCall #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="READ_PHONE_NUMBERS" />
</manifest>
2 changes: 2 additions & 0 deletions android/src/main/java/io/wazo/callkeep/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
13 changes: 10 additions & 3 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -178,19 +181,23 @@ 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);

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);
}
Expand Down Expand Up @@ -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")) {
Expand Down
317 changes: 317 additions & 0 deletions android/src/main/java/io/wazo/callkeep/VideoConnectionProvider.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Loading