diff --git a/android/build.gradle b/android/build.gradle index b21d495..07e88d0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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) { @@ -16,6 +27,10 @@ android { } } +repositories { + mavenCentral() +} + dependencies { compile 'com.facebook.react:react-native:+' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index cc3f093..e857881 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ + + diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index 3db792c..77aa4d3 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -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; @@ -73,6 +62,7 @@ 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"; @@ -80,7 +70,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { 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; @@ -115,7 +105,7 @@ public String getName() { @ReactMethod public void displayIncomingCall(String number, String callerName) { - if (!this.hasPhoneAccount()) { + if (!isAvailable() || !hasPhoneAccount()) { return; } @@ -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; } @@ -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; } @@ -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); } @@ -227,16 +238,22 @@ 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() { @@ -244,15 +261,7 @@ private static boolean hasPhoneAccount() { return false; } - List 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() { @@ -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; } @@ -313,6 +323,9 @@ public void onReceive(Context context, Intent intent) { sendEventToJS("RNCallKeepDidReceiveStartCallAction", args); break; + case ACTION_AUDIO_SESSION: + sendEventToJS("RNCallKeepDidActivateAudioSession", null); + break; } } } diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java index dd5036a..2201c6a 100644 --- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java +++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java @@ -17,12 +17,13 @@ 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; @@ -30,19 +31,20 @@ 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; @@ -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; } @@ -112,6 +116,7 @@ public void onAnswer() { connection.setAudioModeIsVoip(true); sendCallRequestToActivity(ACTION_ANSWER_CALL, null); + sendCallRequestToActivity(ACTION_AUDIO_SESSION, null); } @Override @@ -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); diff --git a/docs/android-installation.md b/docs/android-installation.md index 0d39dc6..0ff8007 100644 --- a/docs/android-installation.md +++ b/docs/android-installation.md @@ -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) { @@ -61,8 +61,10 @@ public class MainActivity extends ReactActivity { ```xml + + - + // ...