Skip to content
Merged
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
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