Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…-native-callkeep into speakerOutputAndroid
  • Loading branch information
vtn-dev-prithipal committed Jan 5, 2021
2 parents be5f3fb + 8bdddcc commit adc6bba
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 62 deletions.
20 changes: 20 additions & 0 deletions MIGRATION_v3_v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Migration from CallKeep v3 to v4

The `reportNewIncomingCall` method on iOS is now more consistent.

Please update your `AppDelegate.m` file with this new signature:

```objc
[RNCallKeep reportNewIncomingCall: uuidString
handle: handle
handleType: handleType
hasVideo: YES
localizedCallerName: localizedCallerName
supportsHolding: YES
supportsDTMF: YES
supportsGrouping: YES
supportsUngrouping: YES
fromPushKit: YES
payload: nil
withCompletionHandler: nil];
```
100 changes: 96 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ const options = {
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
additionalPermissions: [PermissionsAndroid.PERMISSIONS.example]
additionalPermissions: [PermissionsAndroid.PERMISSIONS.example],
// Required to get audio in background when using Android 11
foregroundService: {
channelId: 'com.company.my',
channelName: 'Foreground service for my app',
notificationTitle: 'My app is running on background',
notificationIcon: 'Path to the resource icon of the notification',
},
}
};

Expand Down Expand Up @@ -124,6 +131,30 @@ Eg: When your used log out (or the connection to your server is broken, etc..),
RNCallKeep.setAvailable(true);
```

### setForegroundServiceSettings
_This feature is available only on Android._

Configures the [Foreground Service](https://developer.android.com/about/versions/11/privacy/foreground-services) used for Android 11 to get microphone access on background.
Similar to set the `foregroundService` key in the `setup()` method.

```js
RNCallKeep.setForegroundServiceSettings({
channelId: 'com.company.my',
channelName: 'Foreground service for my app',
notificationTitle: 'My app is running on background',
notificationIcon: 'Path to the resource icon of the notification',
});
```

### canMakeMultipleCalls
_This feature is available only on Android._

Disable the "Add call" button in ConnectionService UI.

```js
RNCallKeep.canMakeMultipleCalls(false); // Enabled by default
```

- `active`: boolean
- Tell whether the app is ready or not

Expand Down Expand Up @@ -177,6 +208,13 @@ RNCallKeep.displayIncomingCall(uuid, handle, localizedCallerName);
- `hasVideo`: boolean (optional, iOS only)
- `false` (default)
- `true` (you know... when not false)
- `options`: object (optional)
- `ios`: object
- `supportsHolding`: boolean (optional, default true)
- `supportsDTMF`: boolean (optional, default true)
- `supportsGrouping`: boolean (optional, default true)
- `supportsUngrouping`: boolean (optional, default true)
- `android`: object (currently no-op)

### answerIncomingCall
_This feature is available only on Android._
Expand Down Expand Up @@ -232,6 +270,14 @@ RNCallKeep.updateDisplay(uuid, displayName, handle)
- Name of the caller to be displayed on the native UI
- `handle`: string
- Phone number of the caller
- `options`: object (optional)
- `ios`: object
- `hasVideo`: boolean (optional)
- `supportsHolding`: boolean (optional)
- `supportsDTMF`: boolean (optional)
- `supportsGrouping`: boolean (optional)
- `supportsUngrouping`: boolean (optional)
- `android`: object (currently no-op)

### endCall

Expand Down Expand Up @@ -388,6 +434,29 @@ const options = {
RNCallKeep.hasDefaultPhoneAccount(options);
```

### checkPhoneAccountEnabled

Checks if the user has set a default [phone account](https://developer.android.com/reference/android/telecom/PhoneAccount) and it's enabled.

It's useful for custom permission prompts. It should be used in pair with `registerPhoneAccount`
Similar to `hasDefaultPhoneAccount` but without trigering a prompt if the user doesn't have a phone account.

_This feature is available only on Android._

```js
RNCallKeep.checkPhoneAccountEnabled();
```

### isConnectionServiceAvailable

Check if the device support ConnectionService.

_This feature is available only on Android._

```js
RNCallKeep.checkPhoneAccountEnabled();
```

### backToForeground
_This feature is available only on Android._

Expand Down Expand Up @@ -538,9 +607,14 @@ Called as soon as JS context initializes if there were some actions performed by

Since iOS 13, you must display incoming call on receiving PushKit push notification. But if app was killed, it takes some time to create JS context. If user answers the call (or ends it) before JS context has been initialized, user actions will be passed as events array of this event. Similar situation can happen if user would like to start a call from Recents or similar iOS app, assuming that your app was in killed state.

**NOTE: You still need to subscribe / handle the rest events as usuall. This is just a helper whcih cache and propagate early fired events if and only if for "the native events which DID fire BEFORE js bridge is initialed", it does NOT mean this will have events each time when the app reopened.**

```js
// register `didLoadWithEvents` somewhere early in your app when it is ready to handle callkeep events.

RNCallKeep.addEventListener('didLoadWithEvents', (events) => {
// see example usage in https://github.com/react-native-webrtc/react-native-callkeep/pull/169
// `events` is passed as an Array chronologically, handle or ignore events based on the app's logic
// see example usage in https://github.com/react-native-webrtc/react-native-callkeep/pull/169 or https://github.com/react-native-webrtc/react-native-callkeep/pull/205
});
```

Expand Down Expand Up @@ -724,7 +798,7 @@ In some case your application can be unreachable :
- when the user kill the application
- when it's in background since a long time (eg: after ~5mn the os will kill all connections).

To be able to wake up your application to display the incoming call, you can use [https://github.com/ianlin/react-native-voip-push-notification](react-native-voip-push-notification) on iOS or BackgroundMessaging from [react-native-firebase](https://rnfirebase.io/docs/v5.x.x/messaging/receiving-messages#4)-(Optional)(Android-only)-Listen-for-FCM-messages-in-the-background).
To be able to wake up your application to display the incoming call, you can use [https://github.com/ianlin/react-native-voip-push-notification](react-native-voip-push-notification) on iOS or BackgroundMessaging from [react-native-firebase](https://rnfirebase.io/messaging/usage#receiving-messages)-(Optional)(Android-only)-Listen-for-FCM-messages-in-the-background).

You have to send a push to your application, like with Firebase for Android and with a library supporting PushKit pushes for iOS.

Expand All @@ -743,10 +817,28 @@ Since iOS 13, you'll have to report the incoming calls that wakes up your applic
// NSString *handle = @"caller number here";
// NSDictionary *extra = [payload.dictionaryPayload valueForKeyPath:@"custom.path.to.data"]; /* use this to pass any special data (ie. from your notification) down to RN. Can also be `nil` */

[RNCallKeep reportNewIncomingCall:uuid handle:handle handleType:@"generic" hasVideo:false localizedCallerName:callerName fromPushKit: YES payload:extra withCompletionHandler:completion];
[RNCallKeep reportNewIncomingCall: uuid
handle: handle
handleType: @"generic"
hasVideo: NO
localizedCallerName: callerName
supportsHolding: YES
supportsDTMF: YES
supportsGrouping: YES
supportsUngrouping: YES
fromPushKit: YES
payload: extra
withCompletionHandler: completion];
}
```

## Android 11

Since Android 11, your application [requires to start a foregroundService](https://developer.android.com/about/versions/11/privacy/foreground-services) in order to access the microphone in background.
You'll need to upgrade your `compileSdkVersion` to `30` to be able to use this feature.

You have to set the `foregroundService` key in the [`setup()`](#setup) method and add a `foregroundServiceType` in the [`AndroidManifest` file](docs/android-installation.md#android-common-step-installation).

## Debug

### Android
Expand Down
4 changes: 3 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.wazo.callkeep">

<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
</manifest>
4 changes: 4 additions & 0 deletions android/src/main/java/io/wazo/callkeep/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public class Constants {
public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER";
public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID";
public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME";
// Can't use telecom.EXTRA_DISABLE_ADD_CALL ...
public static final String EXTRA_DISABLE_ADD_CALL = "android.telecom.extra.DISABLE_ADD_CALL";

public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128;
}
110 changes: 105 additions & 5 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
import com.facebook.react.modules.permissions.PermissionsModule;

import java.lang.reflect.Array;
import java.util.ArrayList;
Expand Down Expand Up @@ -91,8 +93,11 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {

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 final String[] permissions = { Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CALL_PHONE, Manifest.permission.RECORD_AUDIO };
private static final String[] permissions = {
Build.VERSION.SDK_INT < 30 ? Manifest.permission.READ_PHONE_STATE : Manifest.permission.READ_PHONE_NUMBERS,
Manifest.permission.CALL_PHONE,
Manifest.permission.RECORD_AUDIO
};

private static final String TAG = "RNCK:RNCallKeepModule";
private static TelecomManager telecomManager;
Expand Down Expand Up @@ -125,6 +130,8 @@ public void setup(ReadableMap options) {
this.registerEvents();
VoiceConnectionService.setAvailable(true);
}

VoiceConnectionService.setSettings(options);
}

@ReactMethod
Expand Down Expand Up @@ -250,13 +257,83 @@ public void checkPhoneAccountPermission(ReadableArray optionalPermissions, Promi
optionalPermsArr[i] = optionalPermissions.getString(i);
}

String[] allPermissions = Arrays.copyOf(permissions, permissions.length + optionalPermsArr.length);
final String[] allPermissions = Arrays.copyOf(permissions, permissions.length + optionalPermsArr.length);
System.arraycopy(optionalPermsArr, 0, allPermissions, permissions.length, optionalPermsArr.length);

hasPhoneAccountPromise = promise;

if (!this.hasPermissions()) {
requestPermissions(currentActivity, allPermissions, REQUEST_READ_PHONE_STATE);
WritableArray allPermissionaw = Arguments.createArray();
for (String allPermission : allPermissions) {
allPermissionaw.pushString(allPermission);
}

getReactApplicationContext()
.getNativeModule(PermissionsModule.class)
.requestMultiplePermissions(allPermissionaw, new Promise() {
@Override
public void resolve(@Nullable Object value) {
WritableMap grantedPermission = (WritableMap) value;
int[] grantedResult = new int[allPermissions.length];
for (int i=0; i<allPermissions.length; ++i) {
String perm = allPermissions[i];
grantedResult[i] = grantedPermission.getString(perm).equals("granted")
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
}
RNCallKeepModule.onRequestPermissionsResult(REQUEST_READ_PHONE_STATE, allPermissions, grantedResult);
}

@Override
public void reject(String code, String message) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, Throwable throwable) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, String message, Throwable throwable) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(Throwable throwable) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(Throwable throwable, WritableMap userInfo) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, @NonNull WritableMap userInfo) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, Throwable throwable, WritableMap userInfo) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, String message, @NonNull WritableMap userInfo) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String code, String message, Throwable throwable, WritableMap userInfo) {
hasPhoneAccountPromise.resolve(false);
}

@Override
public void reject(String message) {
hasPhoneAccountPromise.resolve(false);
}
});
return;
}

Expand Down Expand Up @@ -399,6 +476,16 @@ public void setAvailable(Boolean active) {
VoiceConnectionService.setAvailable(active);
}

@ReactMethod
public void setForegroundServiceSettings(ReadableMap settings) {
VoiceConnectionService.setSettings(settings);
}

@ReactMethod
public void canMakeMultipleCalls(Boolean allow) {
VoiceConnectionService.setCanMakeMultipleCalls(allow);
}

@ReactMethod
public void setReachable() {
VoiceConnectionService.setReachable();
Expand Down Expand Up @@ -445,12 +532,21 @@ public void openPhoneAccountSettings() {
this.getAppContext().startActivity(intent);
}

@ReactMethod
public static Boolean isConnectionServiceAvailable() {
// PhoneAccount is available since api level 23
return Build.VERSION.SDK_INT >= 23;
}

@ReactMethod
public void isConnectionServiceAvailable(Promise promise) {
promise.resolve(isConnectionServiceAvailable());
}

@ReactMethod
public void checkPhoneAccountEnabled(Promise promise) {
promise.resolve(hasPhoneAccount());
}

@ReactMethod
public void backToForeground() {
Context context = getAppContext();
Expand Down Expand Up @@ -521,6 +617,10 @@ private String getApplicationName(Context appContext) {
private Boolean hasPermissions() {
Activity currentActivity = this.getCurrentActivity();

if (currentActivity == null) {
return false;
}

boolean hasPermissions = true;
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(currentActivity, permission);
Expand Down
Loading

0 comments on commit adc6bba

Please sign in to comment.