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
+ 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
+ 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