Skip to content

Commit adc6bba

Browse files
Merge branch 'master' of https://github.com/react-native-webrtc/react-native-callkeep into speakerOutputAndroid
2 parents be5f3fb + 8bdddcc commit adc6bba

File tree

12 files changed

+413
-62
lines changed

12 files changed

+413
-62
lines changed

MIGRATION_v3_v4.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Migration from CallKeep v3 to v4
2+
3+
The `reportNewIncomingCall` method on iOS is now more consistent.
4+
5+
Please update your `AppDelegate.m` file with this new signature:
6+
7+
```objc
8+
[RNCallKeep reportNewIncomingCall: uuidString
9+
handle: handle
10+
handleType: handleType
11+
hasVideo: YES
12+
localizedCallerName: localizedCallerName
13+
supportsHolding: YES
14+
supportsDTMF: YES
15+
supportsGrouping: YES
16+
supportsUngrouping: YES
17+
fromPushKit: YES
18+
payload: nil
19+
withCompletionHandler: nil];
20+
```

README.md

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ const options = {
4747
cancelButton: 'Cancel',
4848
okButton: 'ok',
4949
imageName: 'phone_account_icon',
50-
additionalPermissions: [PermissionsAndroid.PERMISSIONS.example]
50+
additionalPermissions: [PermissionsAndroid.PERMISSIONS.example],
51+
// Required to get audio in background when using Android 11
52+
foregroundService: {
53+
channelId: 'com.company.my',
54+
channelName: 'Foreground service for my app',
55+
notificationTitle: 'My app is running on background',
56+
notificationIcon: 'Path to the resource icon of the notification',
57+
},
5158
}
5259
};
5360

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

134+
### setForegroundServiceSettings
135+
_This feature is available only on Android._
136+
137+
Configures the [Foreground Service](https://developer.android.com/about/versions/11/privacy/foreground-services) used for Android 11 to get microphone access on background.
138+
Similar to set the `foregroundService` key in the `setup()` method.
139+
140+
```js
141+
RNCallKeep.setForegroundServiceSettings({
142+
channelId: 'com.company.my',
143+
channelName: 'Foreground service for my app',
144+
notificationTitle: 'My app is running on background',
145+
notificationIcon: 'Path to the resource icon of the notification',
146+
});
147+
```
148+
149+
### canMakeMultipleCalls
150+
_This feature is available only on Android._
151+
152+
Disable the "Add call" button in ConnectionService UI.
153+
154+
```js
155+
RNCallKeep.canMakeMultipleCalls(false); // Enabled by default
156+
```
157+
127158
- `active`: boolean
128159
- Tell whether the app is ready or not
129160

@@ -177,6 +208,13 @@ RNCallKeep.displayIncomingCall(uuid, handle, localizedCallerName);
177208
- `hasVideo`: boolean (optional, iOS only)
178209
- `false` (default)
179210
- `true` (you know... when not false)
211+
- `options`: object (optional)
212+
- `ios`: object
213+
- `supportsHolding`: boolean (optional, default true)
214+
- `supportsDTMF`: boolean (optional, default true)
215+
- `supportsGrouping`: boolean (optional, default true)
216+
- `supportsUngrouping`: boolean (optional, default true)
217+
- `android`: object (currently no-op)
180218

181219
### answerIncomingCall
182220
_This feature is available only on Android._
@@ -232,6 +270,14 @@ RNCallKeep.updateDisplay(uuid, displayName, handle)
232270
- Name of the caller to be displayed on the native UI
233271
- `handle`: string
234272
- Phone number of the caller
273+
- `options`: object (optional)
274+
- `ios`: object
275+
- `hasVideo`: boolean (optional)
276+
- `supportsHolding`: boolean (optional)
277+
- `supportsDTMF`: boolean (optional)
278+
- `supportsGrouping`: boolean (optional)
279+
- `supportsUngrouping`: boolean (optional)
280+
- `android`: object (currently no-op)
235281

236282
### endCall
237283

@@ -388,6 +434,29 @@ const options = {
388434
RNCallKeep.hasDefaultPhoneAccount(options);
389435
```
390436

437+
### checkPhoneAccountEnabled
438+
439+
Checks if the user has set a default [phone account](https://developer.android.com/reference/android/telecom/PhoneAccount) and it's enabled.
440+
441+
It's useful for custom permission prompts. It should be used in pair with `registerPhoneAccount`
442+
Similar to `hasDefaultPhoneAccount` but without trigering a prompt if the user doesn't have a phone account.
443+
444+
_This feature is available only on Android._
445+
446+
```js
447+
RNCallKeep.checkPhoneAccountEnabled();
448+
```
449+
450+
### isConnectionServiceAvailable
451+
452+
Check if the device support ConnectionService.
453+
454+
_This feature is available only on Android._
455+
456+
```js
457+
RNCallKeep.checkPhoneAccountEnabled();
458+
```
459+
391460
### backToForeground
392461
_This feature is available only on Android._
393462

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

539608
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.
540609

610+
**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.**
611+
541612
```js
613+
// register `didLoadWithEvents` somewhere early in your app when it is ready to handle callkeep events.
614+
542615
RNCallKeep.addEventListener('didLoadWithEvents', (events) => {
543-
// see example usage in https://github.com/react-native-webrtc/react-native-callkeep/pull/169
616+
// `events` is passed as an Array chronologically, handle or ignore events based on the app's logic
617+
// 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
544618
});
545619
```
546620

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

727-
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).
801+
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).
728802

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

@@ -743,10 +817,28 @@ Since iOS 13, you'll have to report the incoming calls that wakes up your applic
743817
// NSString *handle = @"caller number here";
744818
// 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` */
745819

746-
[RNCallKeep reportNewIncomingCall:uuid handle:handle handleType:@"generic" hasVideo:false localizedCallerName:callerName fromPushKit: YES payload:extra withCompletionHandler:completion];
820+
[RNCallKeep reportNewIncomingCall: uuid
821+
handle: handle
822+
handleType: @"generic"
823+
hasVideo: NO
824+
localizedCallerName: callerName
825+
supportsHolding: YES
826+
supportsDTMF: YES
827+
supportsGrouping: YES
828+
supportsUngrouping: YES
829+
fromPushKit: YES
830+
payload: extra
831+
withCompletionHandler: completion];
747832
}
748833
```
749834

835+
## Android 11
836+
837+
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.
838+
You'll need to upgrade your `compileSdkVersion` to `30` to be able to use this feature.
839+
840+
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).
841+
750842
## Debug
751843

752844
### Android

android/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.wazo.callkeep">
33

44
<uses-permission android:name="android.permission.CALL_PHONE" />
5-
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
5+
<uses-permission android:name="android.permission.READ_PHONE_STATE"
6+
android:maxSdkVersion="29" />
7+
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
68
</manifest>

android/src/main/java/io/wazo/callkeep/Constants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ public class Constants {
1616
public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER";
1717
public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID";
1818
public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME";
19+
// Can't use telecom.EXTRA_DISABLE_ADD_CALL ...
20+
public static final String EXTRA_DISABLE_ADD_CALL = "android.telecom.extra.DISABLE_ADD_CALL";
21+
22+
public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128;
1923
}

android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@
5555
import com.facebook.react.bridge.ReactMethod;
5656
import com.facebook.react.bridge.ReadableArray;
5757
import com.facebook.react.bridge.ReadableMap;
58+
import com.facebook.react.bridge.WritableArray;
5859
import com.facebook.react.bridge.WritableMap;
5960
import com.facebook.react.HeadlessJsTaskService;
6061
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
62+
import com.facebook.react.modules.permissions.PermissionsModule;
6163

6264
import java.lang.reflect.Array;
6365
import java.util.ArrayList;
@@ -91,8 +93,11 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
9193

9294
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
9395
private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep";
94-
private static final String[] permissions = { Manifest.permission.READ_PHONE_STATE,
95-
Manifest.permission.CALL_PHONE, Manifest.permission.RECORD_AUDIO };
96+
private static final String[] permissions = {
97+
Build.VERSION.SDK_INT < 30 ? Manifest.permission.READ_PHONE_STATE : Manifest.permission.READ_PHONE_NUMBERS,
98+
Manifest.permission.CALL_PHONE,
99+
Manifest.permission.RECORD_AUDIO
100+
};
96101

97102
private static final String TAG = "RNCK:RNCallKeepModule";
98103
private static TelecomManager telecomManager;
@@ -125,6 +130,8 @@ public void setup(ReadableMap options) {
125130
this.registerEvents();
126131
VoiceConnectionService.setAvailable(true);
127132
}
133+
134+
VoiceConnectionService.setSettings(options);
128135
}
129136

130137
@ReactMethod
@@ -250,13 +257,83 @@ public void checkPhoneAccountPermission(ReadableArray optionalPermissions, Promi
250257
optionalPermsArr[i] = optionalPermissions.getString(i);
251258
}
252259

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

256263
hasPhoneAccountPromise = promise;
257264

258265
if (!this.hasPermissions()) {
259-
requestPermissions(currentActivity, allPermissions, REQUEST_READ_PHONE_STATE);
266+
WritableArray allPermissionaw = Arguments.createArray();
267+
for (String allPermission : allPermissions) {
268+
allPermissionaw.pushString(allPermission);
269+
}
270+
271+
getReactApplicationContext()
272+
.getNativeModule(PermissionsModule.class)
273+
.requestMultiplePermissions(allPermissionaw, new Promise() {
274+
@Override
275+
public void resolve(@Nullable Object value) {
276+
WritableMap grantedPermission = (WritableMap) value;
277+
int[] grantedResult = new int[allPermissions.length];
278+
for (int i=0; i<allPermissions.length; ++i) {
279+
String perm = allPermissions[i];
280+
grantedResult[i] = grantedPermission.getString(perm).equals("granted")
281+
? PackageManager.PERMISSION_GRANTED
282+
: PackageManager.PERMISSION_DENIED;
283+
}
284+
RNCallKeepModule.onRequestPermissionsResult(REQUEST_READ_PHONE_STATE, allPermissions, grantedResult);
285+
}
286+
287+
@Override
288+
public void reject(String code, String message) {
289+
hasPhoneAccountPromise.resolve(false);
290+
}
291+
292+
@Override
293+
public void reject(String code, Throwable throwable) {
294+
hasPhoneAccountPromise.resolve(false);
295+
}
296+
297+
@Override
298+
public void reject(String code, String message, Throwable throwable) {
299+
hasPhoneAccountPromise.resolve(false);
300+
}
301+
302+
@Override
303+
public void reject(Throwable throwable) {
304+
hasPhoneAccountPromise.resolve(false);
305+
}
306+
307+
@Override
308+
public void reject(Throwable throwable, WritableMap userInfo) {
309+
hasPhoneAccountPromise.resolve(false);
310+
}
311+
312+
@Override
313+
public void reject(String code, @NonNull WritableMap userInfo) {
314+
hasPhoneAccountPromise.resolve(false);
315+
}
316+
317+
@Override
318+
public void reject(String code, Throwable throwable, WritableMap userInfo) {
319+
hasPhoneAccountPromise.resolve(false);
320+
}
321+
322+
@Override
323+
public void reject(String code, String message, @NonNull WritableMap userInfo) {
324+
hasPhoneAccountPromise.resolve(false);
325+
}
326+
327+
@Override
328+
public void reject(String code, String message, Throwable throwable, WritableMap userInfo) {
329+
hasPhoneAccountPromise.resolve(false);
330+
}
331+
332+
@Override
333+
public void reject(String message) {
334+
hasPhoneAccountPromise.resolve(false);
335+
}
336+
});
260337
return;
261338
}
262339

@@ -399,6 +476,16 @@ public void setAvailable(Boolean active) {
399476
VoiceConnectionService.setAvailable(active);
400477
}
401478

479+
@ReactMethod
480+
public void setForegroundServiceSettings(ReadableMap settings) {
481+
VoiceConnectionService.setSettings(settings);
482+
}
483+
484+
@ReactMethod
485+
public void canMakeMultipleCalls(Boolean allow) {
486+
VoiceConnectionService.setCanMakeMultipleCalls(allow);
487+
}
488+
402489
@ReactMethod
403490
public void setReachable() {
404491
VoiceConnectionService.setReachable();
@@ -445,12 +532,21 @@ public void openPhoneAccountSettings() {
445532
this.getAppContext().startActivity(intent);
446533
}
447534

448-
@ReactMethod
449535
public static Boolean isConnectionServiceAvailable() {
450536
// PhoneAccount is available since api level 23
451537
return Build.VERSION.SDK_INT >= 23;
452538
}
453539

540+
@ReactMethod
541+
public void isConnectionServiceAvailable(Promise promise) {
542+
promise.resolve(isConnectionServiceAvailable());
543+
}
544+
545+
@ReactMethod
546+
public void checkPhoneAccountEnabled(Promise promise) {
547+
promise.resolve(hasPhoneAccount());
548+
}
549+
454550
@ReactMethod
455551
public void backToForeground() {
456552
Context context = getAppContext();
@@ -521,6 +617,10 @@ private String getApplicationName(Context appContext) {
521617
private Boolean hasPermissions() {
522618
Activity currentActivity = this.getCurrentActivity();
523619

620+
if (currentActivity == null) {
621+
return false;
622+
}
623+
524624
boolean hasPermissions = true;
525625
for (String permission : permissions) {
526626
int permissionCheck = ContextCompat.checkSelfPermission(currentActivity, permission);

0 commit comments

Comments
 (0)