Skip to content

Commit

Permalink
Merge pull request react-native-webrtc#5 from grit96/master
Browse files Browse the repository at this point in the history
Add ability to start calls on Android
  • Loading branch information
manuquentin authored Jan 29, 2019
2 parents bdfd5de + 7cb4180 commit 06b5c47
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 62 deletions.
15 changes: 15 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
buildscript {
repositories {
jcenter()
google()
}

dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}

apply plugin: 'com.android.library'

def safeExtGet(prop, fallback) {
Expand All @@ -16,6 +27,10 @@ android {
}
}

repositories {
mavenCentral()
}

dependencies {
compile 'com.facebook.react:react-native:+'
}
2 changes: 2 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wazo.callkeep">

<uses-permission android:name="android.permission.CALL_PHONE" />
</manifest>
109 changes: 61 additions & 48 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,47 +17,36 @@

package io.wazo.callkeep;

import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.IntentFilter;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.annotation.Nullable;

import android.accounts.AccountManager;
import android.accounts.Account;
import android.telecom.DisconnectCause;
import android.telecom.Connection;
import android.telecom.PhoneAccountHandle;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;

import android.os.Bundle;
import android.os.Build;
import android.net.Uri;
import android.app.Activity;
import android.Manifest;

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.lang.SecurityException;

// @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 {
public static final int REQUEST_READ_PHONE_STATE = 394858;
Expand All @@ -73,14 +62,15 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
public static final String ACTION_HOLD_CALL = "ACTION_HOLD_CALL";
public static final String ACTION_UNHOLD_CALL = "ACTION_UNHOLD_CALL";
public static final String ACTION_ONGOING_CALL = "ACTION_ONGOING_CALL";
public static final String ACTION_AUDIO_SESSION = "ACTION_AUDIO_SESSION";

private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep";

private static TelecomManager telecomManager;
private static Promise hasPhoneAccountPromise;
private ReactApplicationContext reactContext;
private PhoneAccountHandle pah;
private static PhoneAccountHandle handle;
private boolean isReceiverRegistered = false;
private VoiceBroadcastReceiver voiceBroadcastReceiver;

Expand Down Expand Up @@ -115,7 +105,7 @@ public String getName() {

@ReactMethod
public void displayIncomingCall(String number, String callerName) {
if (!this.hasPhoneAccount()) {
if (!isAvailable() || !hasPhoneAccount()) {
return;
}

Expand All @@ -125,12 +115,30 @@ public void displayIncomingCall(String number, String callerName) {
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
extras.putString(EXTRA_CALLER_NAME, callerName);

telecomManager.addNewIncomingCall(this.pah, extras);
telecomManager.addNewIncomingCall(handle, extras);
}

@ReactMethod
public void startCall(String number, String callerName) {
if (!isAvailable() || !hasPhoneAccount()) {
return;
}

Bundle extras = new Bundle();
Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);

Bundle callExtras = new Bundle();
callExtras.putString(EXTRA_CALLER_NAME, callerName);

extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle);
extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras);

telecomManager.placeCall(uri, extras);
}

@ReactMethod
public void endCall() {
if (!hasPhoneAccount()) {
if (!isAvailable() || !hasPhoneAccount()) {
return;
}

Expand All @@ -156,7 +164,8 @@ public void checkPhoneAccountPermission(Promise promise) {
}

hasPhoneAccountPromise = promise;
if (!this.checkPermission(Manifest.permission.READ_PHONE_STATE, REQUEST_READ_PHONE_STATE)) {
String[] permissions = { Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE };
if (!this.checkPermissions(permissions, REQUEST_READ_PHONE_STATE)) {
return;
}

Expand Down Expand Up @@ -201,17 +210,19 @@ public static Boolean isAvailable() {
}

private void registerPhoneAccount(Context appContext) {
if (!isAvailable()) {
return;
}

ComponentName cName = new ComponentName(this.getAppContext(), VoiceConnectionService.class);
String appName = this.getApplicationName(appContext);

this.pah = new PhoneAccountHandle(cName, appName);
handle = new PhoneAccountHandle(cName, appName);

PhoneAccount account = new PhoneAccount.Builder(pah, appName)
PhoneAccount account = new PhoneAccount.Builder(handle, appName)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build();

PhoneAccountHandle handle = new PhoneAccountHandle(cName, appName);

telecomManager = (TelecomManager) this.getAppContext().getSystemService(this.getAppContext().TELECOM_SERVICE);
telecomManager.registerPhoneAccount(account);
}
Expand All @@ -227,32 +238,30 @@ private String getApplicationName(Context appContext) {
return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : appContext.getString(stringId);
}

private Boolean checkPermission(String name, int id) {
private Boolean checkPermissions(String[] permissions, int id) {
Activity currentActivity = this.getCurrentActivity();
int permissionCheck = ContextCompat.checkSelfPermission(currentActivity, name);

if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(currentActivity, new String[]{name}, id);
return false;
boolean hasPermissions = true;
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(currentActivity, permission);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
hasPermissions = false;
}
}

return true;
if (!hasPermissions) {
ActivityCompat.requestPermissions(currentActivity, permissions, id);
}

return hasPermissions;
}

private static boolean hasPhoneAccount() {
if (!isAvailable()) {
return false;
}

List<PhoneAccountHandle> enabledAccounts = telecomManager.getCallCapablePhoneAccounts();

for (PhoneAccountHandle account : enabledAccounts) {
if (account.getComponentName().getClassName().equals(VoiceConnectionService.class.getCanonicalName())) {
return true;
}
}

return false;
return telecomManager.getPhoneAccount(handle).isEnabled();
}

private void registerReceiver() {
Expand All @@ -266,6 +275,7 @@ private void registerReceiver() {
intentFilter.addAction(ACTION_UNHOLD_CALL);
intentFilter.addAction(ACTION_HOLD_CALL);
intentFilter.addAction(ACTION_ONGOING_CALL);
intentFilter.addAction(ACTION_AUDIO_SESSION);
LocalBroadcastManager.getInstance(this.reactContext).registerReceiver(voiceBroadcastReceiver, intentFilter);
isReceiverRegistered = true;
}
Expand Down Expand Up @@ -313,6 +323,9 @@ public void onReceive(Context context, Intent intent) {

sendEventToJS("RNCallKeepDidReceiveStartCallAction", args);
break;
case ACTION_AUDIO_SESSION:
sendEventToJS("RNCallKeepDidActivateAudioSession", null);
break;
}
}
}
Expand Down
25 changes: 14 additions & 11 deletions android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,34 @@

package io.wazo.callkeep;

import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.os.Handler;
import android.support.annotation.Nullable;

import android.support.v4.content.LocalBroadcastManager;
import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.os.Handler;

import static io.wazo.callkeep.RNCallKeepModule.ACTION_END_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_ANSWER_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_MUTE_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNMUTE_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_AUDIO_SESSION;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_DTMF_TONE;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_END_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_HOLD_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNHOLD_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_MUTE_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_ONGOING_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNHOLD_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNMUTE_CALL;
import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALLER_NAME;

// @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)
public class VoiceConnectionService extends ConnectionService {
private static Connection connection;
private static Boolean isActive = false;
Expand Down Expand Up @@ -76,8 +78,10 @@ public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManage

Connection outgoingCallConnection = createConnection(request);
outgoingCallConnection.setDialing();
outgoingCallConnection.setAudioModeIsVoip(true);

sendCallRequestToActivity(ACTION_ONGOING_CALL, request.getAddress().getSchemeSpecificPart());
sendCallRequestToActivity(ACTION_AUDIO_SESSION, null);

return outgoingCallConnection;
}
Expand Down Expand Up @@ -112,6 +116,7 @@ public void onAnswer() {
connection.setAudioModeIsVoip(true);

sendCallRequestToActivity(ACTION_ANSWER_CALL, null);
sendCallRequestToActivity(ACTION_AUDIO_SESSION, null);
}

@Override
Expand Down Expand Up @@ -176,9 +181,7 @@ public void onReject() {

Bundle extra = request.getExtras();

connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE);
connection.setConnectionCapabilities(Connection.CAPABILITY_HOLD);
connection.setConnectionCapabilities(Connection.CAPABILITY_SUPPORT_HOLD);
connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD);
connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
connection.setExtras(extra);
connection.setCallerDisplayName(extra.getString(EXTRA_CALLER_NAME), TelecomManager.PRESENTATION_ALLOWED);
Expand Down
6 changes: 4 additions & 2 deletions docs/android-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import io.wazo.callkeep.RNCallKeepModule; // Add this import line with others

public class MainActivity extends ReactActivity {
// ...

// Permission results
@Override
public void onRequestPermissionsResult(int permsRequestCode, String[] permissions, int[] grantResults) {
Expand All @@ -61,8 +61,10 @@ public class MainActivity extends ReactActivity {
```xml
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />

<application>
<application>
// ...
<service android:name="io.wazo.callkeep.VoiceConnectionService"
android:label="Wazo"
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class RNCallKeep {

startCall(uuid, handle, handleType = 'number', hasVideo = false, contactIdentifier) {
if (!isIOS) {
// Can't start a call directly on Android
RNCallKeepModule.startCall(handle, contactIdentifier);
return;
}

Expand Down

0 comments on commit 06b5c47

Please sign in to comment.