Skip to content

Commit fbcb3aa

Browse files
committed
Add foregroundService for Android 11.
1 parent 395aaa3 commit fbcb3aa

File tree

11 files changed

+144
-13
lines changed

11 files changed

+144
-13
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ doc/api/
1919
*.js_
2020
*.js.deps
2121
*.js.map
22+
.DS_Store
23+
.idea
24+
.vscode
25+
android/gradle*
26+
example/ios/Podfile.lock

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Callkeep acts as an intermediate between your call system (RTC, VOIP...) and the
1515

1616
This allows you (for example) to answer calls when your device is locked even if your app is terminated.
1717

18-
1918
## Initial setup
2019

2120
Basic configuration. In Android a popup is displayed before starting requesting some permissions to work properly.
@@ -31,6 +30,13 @@ final callSetup = <String, dynamic>{
3130
'This application needs to access your phone accounts',
3231
'cancelButton': 'Cancel',
3332
'okButton': 'ok',
33+
// Required to get audio in background when using Android 11
34+
'foregroundService': {
35+
'channelId': 'com.company.my',
36+
'channelName': 'Foreground service for my app',
37+
'notificationTitle': 'My app is running on background',
38+
'notificationIcon': 'Path to the resource icon of the notification',
39+
},
3440
},
3541
};
3642

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public class CallKeepModule {
7979
private static final String TAG = "FLT:CallKeepModule";
8080
private static TelecomManager telecomManager;
8181
private static TelephonyManager telephonyManager;
82-
private static MethodChannel.Result hasPhoneAccountPromise;
8382
private Context _context;
8483
public static PhoneAccountHandle handle;
8584
private boolean isReceiverRegistered = false;
@@ -207,6 +206,11 @@ public boolean HandleMethodCall(@NonNull MethodCall call, @NonNull Result result
207206
backToForeground(result);
208207
}
209208
break;
209+
case "foregroundService": {
210+
VoiceConnectionService.setSettings(new ConstraintsMap((Map<String, Object>)call.argument("settings")));
211+
result.success(null);
212+
}
213+
break;
210214
default:
211215
return false;
212216
}
@@ -222,8 +226,9 @@ public void setup(ConstraintsMap options) {
222226
this.registerEvents();
223227
VoiceConnectionService.setAvailable(true);
224228
}
225-
}
226229

230+
VoiceConnectionService.setSettings(options);
231+
}
227232

228233
public void registerPhoneAccount() {
229234
if (!isConnectionServiceAvailable()) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ 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+
20+
public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128;
1921
}

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
import android.annotation.TargetApi;
2121
import android.app.ActivityManager;
2222
import android.app.ActivityManager.RunningTaskInfo;
23+
import android.app.Notification;
24+
import android.app.NotificationChannel;
25+
import android.app.NotificationManager;
2326
import android.content.ComponentName;
2427
import android.content.Context;
2528
import android.content.Intent;
29+
import android.content.res.Resources;
2630
import android.net.Uri;
2731
import android.os.Build;
2832
import android.os.Bundle;
@@ -31,11 +35,14 @@
3135
import android.telecom.ConnectionRequest;
3236
import android.telecom.ConnectionService;
3337
import android.telecom.DisconnectCause;
38+
import android.telecom.PhoneAccount;
3439
import android.telecom.PhoneAccountHandle;
3540
import android.telecom.TelecomManager;
3641
import android.util.Log;
3742

3843
import androidx.annotation.Nullable;
44+
import androidx.annotation.RequiresApi;
45+
import androidx.core.app.NotificationCompat;
3946
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
4047

4148
import java.util.ArrayList;
@@ -46,7 +53,9 @@
4653
import java.util.Set;
4754
import java.util.UUID;
4855

56+
import io.wazo.callkeep.utils.ConstraintsMap;
4957
import static io.wazo.callkeep.Constants.*;
58+
import static io.wazo.callkeep.Constants.FOREGROUND_SERVICE_TYPE_MICROPHONE;
5059

5160
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionService.java
5261
@TargetApi(Build.VERSION_CODES.M)
@@ -61,6 +70,7 @@ public class VoiceConnectionService extends ConnectionService {
6170
public static Map<String, VoiceConnection> currentConnections = new HashMap<>();
6271
public static Boolean hasOutgoingCall = false;
6372
public static VoiceConnectionService currentConnectionService = null;
73+
public static ConstraintsMap _settings = null;
6474

6575
public static Connection getConnection(String connectionId) {
6676
if (currentConnections.containsKey(connectionId)) {
@@ -92,6 +102,10 @@ public static void setAvailable(Boolean value) {
92102
isAvailable = value;
93103
}
94104

105+
public static void setSettings(ConstraintsMap settings) {
106+
_settings = settings;
107+
}
108+
95109
public static void setReachable() {
96110
Log.d(TAG, "setReachable");
97111
isReachable = true;
@@ -102,6 +116,8 @@ public static void deinitConnection(String connectionId) {
102116
Log.d(TAG, "deinitConnection:" + connectionId);
103117
VoiceConnectionService.hasOutgoingCall = false;
104118

119+
currentConnectionService.stopForegroundService();
120+
105121
if (currentConnections.containsKey(connectionId)) {
106122
currentConnections.remove(connectionId);
107123
}
@@ -116,6 +132,8 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage
116132
incomingCallConnection.setRinging();
117133
incomingCallConnection.setInitialized();
118134

135+
startForegroundService();
136+
119137
return incomingCallConnection;
120138
}
121139

@@ -164,6 +182,8 @@ private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Bool
164182
outgoingCallConnection.setAudioModeIsVoip(true);
165183
outgoingCallConnection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED);
166184

185+
startForegroundService();
186+
167187
// ‍️Weirdly on some Samsung phones (A50, S9...) using `setInitialized` will not display the native UI ...
168188
// when making a call from the native Phone application. The call will still be displayed correctly without it.
169189
if (!Build.MANUFACTURER.equalsIgnoreCase("Samsung")) {
@@ -180,6 +200,54 @@ private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Bool
180200
return outgoingCallConnection;
181201
}
182202

203+
private void startForegroundService() {
204+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
205+
// Foreground services not required before SDK 28
206+
return;
207+
}
208+
Log.d(TAG, "[VoiceConnectionService] startForegroundService");
209+
if (_settings == null || !_settings.hasKey("foregroundService")) {
210+
Log.w(TAG, "[VoiceConnectionService] Not creating foregroundService because not configured");
211+
return;
212+
}
213+
ConstraintsMap foregroundSettings = _settings.getMap("foregroundService");
214+
String NOTIFICATION_CHANNEL_ID = foregroundSettings.getString("channelId");
215+
String channelName = foregroundSettings.getString("channelName");
216+
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
217+
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
218+
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
219+
assert manager != null;
220+
manager.createNotificationChannel(chan);
221+
222+
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
223+
notificationBuilder.setOngoing(true)
224+
.setContentTitle(foregroundSettings.getString("notificationTitle"))
225+
.setPriority(NotificationManager.IMPORTANCE_MIN)
226+
.setCategory(Notification.CATEGORY_SERVICE);
227+
228+
if (foregroundSettings.hasKey("notificationIcon")) {
229+
Context context = this.getApplicationContext();
230+
Resources res = context.getResources();
231+
String smallIcon = foregroundSettings.getString("notificationIcon");
232+
notificationBuilder.setSmallIcon(res.getIdentifier(smallIcon, "mipmap", context.getPackageName()));
233+
}
234+
235+
Log.d(TAG, "[VoiceConnectionService] Starting foreground service");
236+
237+
Notification notification = notificationBuilder.build();
238+
startForeground(FOREGROUND_SERVICE_TYPE_MICROPHONE, notification);
239+
}
240+
241+
@RequiresApi(api = Build.VERSION_CODES.N)
242+
private void stopForegroundService() {
243+
Log.d(TAG, "[VoiceConnectionService] stopForegroundService");
244+
if (_settings == null || !_settings.hasKey("foregroundService")) {
245+
Log.d(TAG, "[VoiceConnectionService] Discarding stop foreground service, no service configured");
246+
return;
247+
}
248+
stopForeground(FOREGROUND_SERVICE_TYPE_MICROPHONE);
249+
}
250+
183251
private void wakeUpApplication(String uuid, String number, String displayName) {
184252
Intent headlessIntent = new Intent(
185253
this.getApplicationContext(),
@@ -233,6 +301,22 @@ private Connection createConnection(ConnectionRequest request) {
233301
extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString());
234302
VoiceConnection connection = new VoiceConnection(this, extrasMap);
235303
connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
304+
305+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
306+
Context context = getApplicationContext();
307+
TelecomManager telecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE);
308+
PhoneAccount phoneAccount = telecomManager.getPhoneAccount(request.getAccountHandle());
309+
310+
//If the phone account is self managed, then this connection must also be self managed.
311+
if((phoneAccount.getCapabilities() & PhoneAccount.CAPABILITY_SELF_MANAGED) == PhoneAccount.CAPABILITY_SELF_MANAGED) {
312+
Log.d(TAG, "[VoiceConnectionService] PhoneAccount is SELF_MANAGED, so connection will be too");
313+
connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
314+
}
315+
else {
316+
Log.d(TAG, "[VoiceConnectionService] PhoneAccount is not SELF_MANAGED, so connection won't be either");
317+
}
318+
}
319+
236320
connection.setInitializing();
237321
connection.setExtras(extras);
238322
currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection);

example/android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies {
3232
}
3333

3434
android {
35-
compileSdkVersion 28
35+
compileSdkVersion 30
3636

3737
lintOptions {
3838
disable 'InvalidPackage'
@@ -42,7 +42,7 @@ android {
4242
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
4343
applicationId "com.github.cloudwebrtc.flutter_callkeep_example"
4444
minSdkVersion 23
45-
targetSdkVersion 28
45+
targetSdkVersion 30
4646
versionCode flutterVersionCode.toInteger()
4747
versionName flutterVersionName
4848
}

example/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
android:value="2" />
6767
<service android:name="io.wazo.callkeep.VoiceConnectionService"
6868
android:label="Wazo"
69+
android:foregroundServiceType="camera|microphone"
6970
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
7071
<intent-filter>
7172
<action android:name="android.telecom.ConnectionService" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include ':app'

example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/lib/main.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
5454
print('backgroundMessage: CallKeepPerformEndCallAction ${event.callUUID}');
5555
});
5656
if (!_callKeepInited) {
57-
_callKeep.setup(<String, dynamic>{
57+
_callKeep.setup(null, <String, dynamic>{
5858
'ios': {
5959
'appName': 'CallKeepDemo',
6060
},
@@ -64,6 +64,12 @@ Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
6464
'This application needs to access your phone accounts',
6565
'cancelButton': 'Cancel',
6666
'okButton': 'ok',
67+
'foregroundService': {
68+
'channelId': 'com.company.my',
69+
'channelName': 'Foreground service for my app',
70+
'notificationTitle': 'My app is running on background',
71+
'notificationIcon': 'Path to the resource icon of the notification',
72+
},
6773
},
6874
});
6975
_callKeepInited = true;
@@ -263,6 +269,12 @@ class _MyAppState extends State<HomePage> {
263269
'This application needs to access your phone accounts',
264270
'cancelButton': 'Cancel',
265271
'okButton': 'ok',
272+
'foregroundService': {
273+
'channelId': 'com.company.my',
274+
'channelName': 'Foreground service for my app',
275+
'notificationTitle': 'My app is running on background',
276+
'notificationIcon': 'Path to the resource icon of the notification',
277+
},
266278
});
267279
}
268280

@@ -298,7 +310,7 @@ class _MyAppState extends State<HomePage> {
298310
_callKeep.on(CallKeepPerformEndCallAction(), endCall);
299311
_callKeep.on(CallKeepPushKitToken(), onPushKitToken);
300312

301-
_callKeep.setup(<String, dynamic>{
313+
_callKeep.setup(context, <String, dynamic>{
302314
'ios': {
303315
'appName': 'CallKeepDemo',
304316
},

0 commit comments

Comments
 (0)