Skip to content

Start a foreground service to be able to get audio on Android 11 bg #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 23, 2020
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
30 changes: 29 additions & 1 deletion 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',
notiticationIcon: 'Path to the resource icon of the notification',
},
}
};

Expand Down Expand Up @@ -124,6 +131,21 @@ 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',
notiticationIcon: 'Path to the resource icon of the notification',
});
```

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

Expand Down Expand Up @@ -805,6 +827,12 @@ Since iOS 13, you'll have to report the incoming calls that wakes up your applic
}
```

## 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 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
7 changes: 7 additions & 0 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ public void setup(ReadableMap options) {
this.registerEvents();
VoiceConnectionService.setAvailable(true);
}

VoiceConnectionService.setSettings(options);
}

@ReactMethod
Expand Down Expand Up @@ -389,6 +391,11 @@ 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);
Expand Down
56 changes: 53 additions & 3 deletions android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
package io.wazo.callkeep;

import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.res.Resources;
import android.content.Intent;
import android.content.Context;
import android.content.ComponentName;
Expand All @@ -27,6 +33,7 @@
import android.os.Handler;
import android.speech.tts.Voice;
import androidx.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.telecom.CallAudioState;
import android.telecom.Connection;
Expand All @@ -37,10 +44,8 @@
import android.telecom.TelecomManager;
import android.util.Log;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;

import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.ReadableMap;

import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -51,6 +56,7 @@
import java.util.UUID;
import java.util.stream.Collectors;

import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
import static io.wazo.callkeep.Constants.ACTION_AUDIO_SESSION;
import static io.wazo.callkeep.Constants.ACTION_ONGOING_CALL;
import static io.wazo.callkeep.Constants.ACTION_CHECK_REACHABILITY;
Expand All @@ -70,6 +76,7 @@ public class VoiceConnectionService extends ConnectionService {
private static String notReachableCallUuid;
private static ConnectionRequest currentConnectionRequest;
private static PhoneAccountHandle phoneAccountHandle;
private static ReadableMap _settings;
private static String TAG = "RNCK:VoiceConnectionService";
public static Map<String, VoiceConnection> currentConnections = new HashMap<>();
public static Boolean hasOutgoingCall = false;
Expand Down Expand Up @@ -105,6 +112,10 @@ public static void setAvailable(Boolean value) {
isAvailable = value;
}

public static void setSettings(ReadableMap settings) {
_settings = settings;
}

public static void setCanMakeMultipleCalls(Boolean allow) {
VoiceConnectionService.canMakeMultipleCalls = allow;
}
Expand Down Expand Up @@ -133,6 +144,8 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage
incomingCallConnection.setRinging();
incomingCallConnection.setInitialized();

startForegroundService();

return incomingCallConnection;
}

Expand Down Expand Up @@ -185,6 +198,8 @@ private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Bool
outgoingCallConnection.setAudioModeIsVoip(true);
outgoingCallConnection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED);

startForegroundService();

// ‍️Weirdly on some Samsung phones (A50, S9...) using `setInitialized` will not display the native UI ...
// when making a call from the native Phone application. The call will still be displayed correctly without it.
if (!Build.MANUFACTURER.equalsIgnoreCase("Samsung")) {
Expand All @@ -201,6 +216,41 @@ private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Bool
return outgoingCallConnection;
}

private void startForegroundService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Foreground services not required before SDK 28
return;
}
if (_settings == null || !_settings.hasKey("foregroundService")) {
Log.d(TAG, "Not creating foregroundService because not configured");
return;
}
ReadableMap foregroundSettings = _settings.getMap("foregroundService");
String NOTIFICATION_CHANNEL_ID = foregroundSettings.getString("channelId");
String channelName = foregroundSettings.getString("channelName");
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert manager != null;
manager.createNotificationChannel(chan);

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setOngoing(true)
.setContentTitle(foregroundSettings.getString("notificationTitle"))
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE);

if (foregroundSettings.hasKey("notificationIcon")) {
Context context = this.getApplicationContext();
Resources res = context.getResources();
String smallIcon = foregroundSettings.getString("notificationIcon");
notificationBuilder.setSmallIcon(res.getIdentifier(smallIcon, "mipmap", context.getPackageName()));
}

Notification notification = notificationBuilder.build();
startForeground(FOREGROUND_SERVICE_TYPE_MICROPHONE, notification);
}

private void wakeUpApplication(String uuid, String number, String displayName) {
Intent headlessIntent = new Intent(
this.getApplicationContext(),
Expand Down
3 changes: 2 additions & 1 deletion docs/android-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public class MainActivity extends ReactActivity {
// ...
<service android:name="io.wazo.callkeep.VoiceConnectionService"
android:label="Wazo"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:foregroundServiceType="camera|microphone">>
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ declare module 'react-native-callkeep' {
*/
static setAvailable(active: boolean): void

static setForegroundServiceSettings(settings: Object): void

static canMakeMultipleCalls(allow: boolean): void

static setCurrentCallActive(callUUID: string): void
Expand Down
10 changes: 10 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ class RNCallKeep {
RNCallKeepModule.setAvailable(state);
};

setForegroundServiceSettings = (settings) => {
if (isIOS) {
return;
}

RNCallKeepModule.setForegroundServiceSettings({
foregroundService: settings,
});
};

canMakeMultipleCalls = (state) => {
if (isIOS) {
return;
Expand Down