2020import android .annotation .TargetApi ;
2121import android .app .ActivityManager ;
2222import android .app .ActivityManager .RunningTaskInfo ;
23+ import android .app .Notification ;
24+ import android .app .NotificationChannel ;
25+ import android .app .NotificationManager ;
2326import android .content .ComponentName ;
2427import android .content .Context ;
2528import android .content .Intent ;
29+ import android .content .res .Resources ;
2630import android .net .Uri ;
2731import android .os .Build ;
2832import android .os .Bundle ;
3135import android .telecom .ConnectionRequest ;
3236import android .telecom .ConnectionService ;
3337import android .telecom .DisconnectCause ;
38+ import android .telecom .PhoneAccount ;
3439import android .telecom .PhoneAccountHandle ;
3540import android .telecom .TelecomManager ;
3641import android .util .Log ;
3742
3843import androidx .annotation .Nullable ;
44+ import androidx .annotation .RequiresApi ;
45+ import androidx .core .app .NotificationCompat ;
3946import androidx .localbroadcastmanager .content .LocalBroadcastManager ;
4047
4148import java .util .ArrayList ;
4653import java .util .Set ;
4754import java .util .UUID ;
4855
56+ import io .wazo .callkeep .utils .ConstraintsMap ;
4957import 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 );
0 commit comments