1+ package com .hoxfon .react .RNTwilioVoice ;
2+
3+ import android .annotation .TargetApi ;
4+ import android .app .Notification ;
5+ import android .app .NotificationChannel ;
6+ import android .app .NotificationManager ;
7+ import android .app .PendingIntent ;
8+ import android .app .Service ;
9+ import android .content .Context ;
10+ import android .content .Intent ;
11+ import android .graphics .Color ;
12+ import android .os .Build ;
13+ import android .os .Bundle ;
14+ import android .os .IBinder ;
15+ import android .util .Log ;
16+
17+ import androidx .core .app .NotificationCompat ;
18+ import androidx .lifecycle .Lifecycle ;
19+ import androidx .lifecycle .ProcessLifecycleOwner ;
20+ import androidx .localbroadcastmanager .content .LocalBroadcastManager ;
21+
22+ import com .twilio .voice .CallInvite ;
23+
24+ import static com .hoxfon .react .RNTwilioVoice .CallNotificationManager .getMainActivityClass ;
25+ import static com .hoxfon .react .RNTwilioVoice .Constants .ACTION_ACCEPT ;
26+ import static com .hoxfon .react .RNTwilioVoice .Constants .ACTION_CANCEL_CALL ;
27+ import static com .hoxfon .react .RNTwilioVoice .Constants .ACTION_INCOMING_CALL ;
28+ import static com .hoxfon .react .RNTwilioVoice .Constants .ACTION_INCOMING_CALL_NOTIFICATION ;
29+ import static com .hoxfon .react .RNTwilioVoice .Constants .ACTION_REJECT ;
30+ import static com .hoxfon .react .RNTwilioVoice .Constants .CALL_SID_KEY ;
31+ import static com .hoxfon .react .RNTwilioVoice .Constants .INCOMING_CALL_INVITE ;
32+ import static com .hoxfon .react .RNTwilioVoice .Constants .INCOMING_CALL_NOTIFICATION_ID ;
33+ import static com .hoxfon .react .RNTwilioVoice .TwilioVoiceModule .TAG ;
34+
35+ public class IncomingCallNotificationService extends Service {
36+
37+ @ Override
38+ public int onStartCommand (Intent intent , int flags , int startId ) {
39+ String action = intent .getAction ();
40+
41+ CallInvite callInvite = intent .getParcelableExtra (INCOMING_CALL_INVITE );
42+ int notificationId = intent .getIntExtra (INCOMING_CALL_NOTIFICATION_ID , 0 );
43+
44+ switch (action ) {
45+ case ACTION_INCOMING_CALL :
46+ handleIncomingCall (callInvite , notificationId );
47+ break ;
48+ case ACTION_ACCEPT :
49+ accept (callInvite , notificationId );
50+ break ;
51+ case ACTION_REJECT :
52+ reject (callInvite );
53+ break ;
54+ case ACTION_CANCEL_CALL :
55+ handleCancelledCall (intent );
56+ break ;
57+ default :
58+ break ;
59+ }
60+ return START_NOT_STICKY ;
61+ }
62+
63+ @ Override
64+ public IBinder onBind (Intent intent ) {
65+ return null ;
66+ }
67+
68+ private Notification createNotification (CallInvite callInvite , int notificationId , int channelImportance ) {
69+ Intent intent = new Intent (this , getMainActivityClass (this ));
70+ intent .setAction (ACTION_INCOMING_CALL_NOTIFICATION );
71+ intent .putExtra (INCOMING_CALL_NOTIFICATION_ID , notificationId );
72+ intent .putExtra (INCOMING_CALL_INVITE , callInvite );
73+ intent .addFlags (Intent .FLAG_ACTIVITY_CLEAR_TOP );
74+ PendingIntent pendingIntent =
75+ PendingIntent .getActivity (this , notificationId , intent , PendingIntent .FLAG_UPDATE_CURRENT );
76+ /*
77+ * Pass the notification id and call sid to use as an identifier to cancel the
78+ * notification later
79+ */
80+ Bundle extras = new Bundle ();
81+ extras .putString (CALL_SID_KEY , callInvite .getCallSid ());
82+
83+ String contextText = callInvite .getFrom () + " is calling." ;
84+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
85+ // TODO make text configurable from app resources
86+ return buildNotification (contextText ,
87+ pendingIntent ,
88+ extras ,
89+ callInvite ,
90+ notificationId ,
91+ createChannel (channelImportance ));
92+ } else {
93+ return new NotificationCompat .Builder (this )
94+ .setSmallIcon (R .drawable .ic_call_white_24dp )
95+ .setContentTitle (getString (R .string .call_incoming ))
96+ .setContentText (contextText )
97+ .setAutoCancel (true )
98+ .setExtras (extras )
99+ .setContentIntent (pendingIntent )
100+ .setGroup ("test_app_notification" )
101+ .setColor (Color .rgb (214 , 10 , 37 ))
102+ .build ();
103+ }
104+ }
105+
106+ /**
107+ * Build a notification.
108+ *
109+ * @param text the text of the notification
110+ * @param pendingIntent the body, pending intent for the notification
111+ * @param extras extras passed with the notification
112+ * @return the builder
113+ */
114+ @ TargetApi (Build .VERSION_CODES .O )
115+ private Notification buildNotification (String text ,
116+ PendingIntent pendingIntent ,
117+ Bundle extras ,
118+ final CallInvite callInvite ,
119+ int notificationId ,
120+ String channelId ) {
121+ Intent rejectIntent = new Intent (getApplicationContext (), IncomingCallNotificationService .class );
122+ rejectIntent .setAction (ACTION_REJECT );
123+ rejectIntent .putExtra (INCOMING_CALL_INVITE , callInvite );
124+ rejectIntent .putExtra (INCOMING_CALL_NOTIFICATION_ID , notificationId );
125+ PendingIntent piRejectIntent = PendingIntent .getService (getApplicationContext (), 0 , rejectIntent , PendingIntent .FLAG_UPDATE_CURRENT );
126+
127+ Intent acceptIntent = new Intent (getApplicationContext (), IncomingCallNotificationService .class );
128+ acceptIntent .setAction (ACTION_ACCEPT );
129+ acceptIntent .putExtra (INCOMING_CALL_INVITE , callInvite );
130+ acceptIntent .putExtra (INCOMING_CALL_NOTIFICATION_ID , notificationId );
131+ PendingIntent piAcceptIntent = PendingIntent .getService (getApplicationContext (), 0 , acceptIntent , PendingIntent .FLAG_UPDATE_CURRENT );
132+
133+ Notification .Builder builder =
134+ new Notification .Builder (getApplicationContext (), channelId )
135+ .setSmallIcon (R .drawable .ic_call_white_24dp )
136+ .setContentTitle (getString (R .string .call_incoming ))
137+ .setContentText (text )
138+ .setCategory (Notification .CATEGORY_CALL )
139+ .setExtras (extras )
140+ .setAutoCancel (true )
141+ .addAction (android .R .drawable .ic_menu_delete , getString (R .string .decline ), piRejectIntent )
142+ .addAction (android .R .drawable .ic_menu_call , getString (R .string .answer ), piAcceptIntent )
143+ .setFullScreenIntent (pendingIntent , true );
144+
145+ return builder .build ();
146+ }
147+
148+ @ TargetApi (Build .VERSION_CODES .O )
149+ private String createChannel (int channelImportance ) {
150+ String channelId = Constants .VOICE_CHANNEL_HIGH_IMPORTANCE ;
151+ if (channelImportance == NotificationManager .IMPORTANCE_LOW ) {
152+ channelId = Constants .VOICE_CHANNEL_LOW_IMPORTANCE ;
153+ }
154+ NotificationChannel callInviteChannel = new NotificationChannel (channelId ,
155+ "Incoming calls" , channelImportance );
156+ callInviteChannel .setLightColor (Color .GREEN );
157+ // TODO set sound for background incoming call
158+ // callInviteChannel.setSound();
159+ callInviteChannel .setLockscreenVisibility (Notification .VISIBILITY_PUBLIC );
160+ NotificationManager notificationManager = (NotificationManager ) getSystemService (Context .NOTIFICATION_SERVICE );
161+ notificationManager .createNotificationChannel (callInviteChannel );
162+
163+ return channelId ;
164+ }
165+
166+ private void accept (CallInvite callInvite , int notificationId ) {
167+ endForeground ();
168+ Intent activeCallIntent = new Intent (this , getMainActivityClass (this ));
169+ activeCallIntent .addFlags (Intent .FLAG_ACTIVITY_SINGLE_TOP );
170+ activeCallIntent .addFlags (Intent .FLAG_ACTIVITY_NEW_TASK );
171+ activeCallIntent .putExtra (INCOMING_CALL_INVITE , callInvite );
172+ activeCallIntent .putExtra (INCOMING_CALL_NOTIFICATION_ID , notificationId );
173+ activeCallIntent .setAction (ACTION_ACCEPT );
174+ this .startActivity (activeCallIntent );
175+ }
176+
177+ private void reject (CallInvite callInvite ) {
178+ endForeground ();
179+ callInvite .reject (getApplicationContext ());
180+ }
181+
182+ private void handleCancelledCall (Intent intent ) {
183+ endForeground ();
184+ LocalBroadcastManager .getInstance (this ).sendBroadcast (intent );
185+ }
186+
187+ private void handleIncomingCall (CallInvite callInvite , int notificationId ) {
188+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
189+ setCallInProgressNotification (callInvite , notificationId );
190+ }
191+ sendCallInviteToActivity (callInvite , notificationId );
192+ }
193+
194+ private void endForeground () {
195+ stopForeground (true );
196+ }
197+
198+ private void setCallInProgressNotification (CallInvite callInvite , int notificationId ) {
199+ int importance = NotificationManager .IMPORTANCE_LOW ;
200+ if (!isAppVisible ()) {
201+ Log .i (TAG , "setCallInProgressNotification - app is NOT visible." );
202+ importance = NotificationManager .IMPORTANCE_HIGH ;
203+ }
204+ this .startForeground (notificationId , createNotification (callInvite , notificationId , importance ));
205+ }
206+
207+ /*
208+ * Send the CallInvite to the Activity. Start the activity if it is not running already.
209+ */
210+ private void sendCallInviteToActivity (CallInvite callInvite , int notificationId ) {
211+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q && !isAppVisible ()) {
212+ return ;
213+ }
214+ Intent intent = new Intent (this , getMainActivityClass (this ));
215+ intent .setAction (ACTION_INCOMING_CALL );
216+ intent .putExtra (INCOMING_CALL_NOTIFICATION_ID , notificationId );
217+ intent .putExtra (INCOMING_CALL_INVITE , callInvite );
218+ intent .addFlags (Intent .FLAG_ACTIVITY_SINGLE_TOP );
219+ intent .addFlags (Intent .FLAG_ACTIVITY_NEW_TASK );
220+ this .startActivity (intent );
221+ }
222+
223+ private boolean isAppVisible () {
224+ return ProcessLifecycleOwner
225+ .get ()
226+ .getLifecycle ()
227+ .getCurrentState ()
228+ .isAtLeast (Lifecycle .State .STARTED );
229+ }
230+ }
0 commit comments