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