1616 */
1717
1818package io .wazo .callkeep ;
19-
19+ import com . facebook . react . bridge . LifecycleEventListener ;
2020import android .Manifest ;
2121import android .app .Activity ;
2222import android .content .BroadcastReceiver ;
4848import android .telecom .PhoneAccountHandle ;
4949import android .telecom .TelecomManager ;
5050import android .telephony .TelephonyManager ;
51+ import android .telephony .TelephonyCallback ;
52+ import android .telephony .PhoneStateListener ;
5153import android .util .Log ;
5254
5355import com .facebook .react .bridge .Arguments ;
101103import static io .wazo .callkeep .Constants .ACTION_DID_CHANGE_AUDIO_ROUTE ;
102104
103105// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java
104- public class RNCallKeepModule extends ReactContextBaseJavaModule {
106+ public class RNCallKeepModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
105107 public static final int REQUEST_READ_PHONE_STATE = 1337 ;
106108 public static final int REQUEST_REGISTER_CALL_PROVIDER = 394859 ;
107109
@@ -117,6 +119,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
117119
118120 private static final String TAG = "RNCallKeep" ;
119121 private static TelecomManager telecomManager ;
122+ private LegacyCallStateListener legacyCallStateListener ;
123+ private CallStateListener callStateListener ;
120124 private static TelephonyManager telephonyManager ;
121125 private static Promise hasPhoneAccountPromise ;
122126 private ReactApplicationContext reactContext ;
@@ -126,6 +130,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
126130 private static WritableMap _settings ;
127131 private WritableNativeArray delayedEvents ;
128132 private boolean hasListeners = false ;
133+ private boolean hasActiveCall = false ;
129134
130135 public static RNCallKeepModule getInstance (ReactApplicationContext reactContext , boolean realContext ) {
131136 if (instance == null ) {
@@ -150,6 +155,8 @@ public static WritableMap getSettings(@Nullable Context context) {
150155
151156 private RNCallKeepModule (ReactApplicationContext reactContext ) {
152157 super (reactContext );
158+ // This line for listening to the Activity Lifecycle Events so we can end the calls onDestroy
159+ reactContext .addLifecycleEventListener (this );
153160 Log .d (TAG , "[RNCallKeepModule] constructor" );
154161
155162 this .reactContext = reactContext ;
@@ -217,6 +224,125 @@ public void initializeTelecomManager() {
217224 telecomManager = (TelecomManager ) context .getSystemService (Context .TELECOM_SERVICE );
218225 }
219226
227+
228+
229+ /**
230+ * Monitors and logs phone call activities, and shows the phone state
231+ */
232+ private class LegacyCallStateListener extends PhoneStateListener {
233+
234+ @ Override
235+ public void onCallStateChanged (int state , String incomingNumber ) {
236+ switch (state ) {
237+ case TelephonyManager .CALL_STATE_RINGING :
238+ // Incoming call is ringing (not used for outgoing call).
239+ Log .i ("onCallStateChanged" , "CALL_STATE_RINGING" );
240+ break ;
241+ case TelephonyManager .CALL_STATE_OFFHOOK :
242+ // Phone call is active -- off the hook.
243+ // Check if there is active call in native
244+ boolean isInManagedCall = RNCallKeepModule .this .checkIsInManagedCall ();
245+
246+ // Only let the JS side know if there is active app call & active native call
247+ if (RNCallKeepModule .this .hasActiveCall && isInManagedCall ){
248+ WritableMap args = Arguments .createMap ();
249+ RNCallKeepModule .this .sendEventToJS ("RNCallKeepHasActiveCall" ,args );
250+ }else if (VoiceConnectionService .currentConnections .size () > 0 ){
251+ // Will enter here for the first time to mark the app has active call
252+ RNCallKeepModule .this .hasActiveCall = true ;
253+ }
254+ Log .i ("onCallStateChanged" , "CALL_STATE_OFFHOOK" );
255+ break ;
256+ case TelephonyManager .CALL_STATE_IDLE :
257+ // Phone is idle before and after phone call.
258+ // If running on version older than 19 (KitKat),
259+ // restart activity when phone call ends.
260+ Log .i ("onCallStateChanged" , "CALL_STATE_IDLE" );
261+ break ;
262+ default :
263+ Log .i ("onCallStateChanged" , "default" );
264+ break ;
265+ }
266+ }
267+ }
268+
269+ private class CallStateListener extends TelephonyCallback implements TelephonyCallback .CallStateListener {
270+
271+ @ Override
272+ public void onCallStateChanged (int state ) {
273+ switch (state ) {
274+ case TelephonyManager .CALL_STATE_RINGING :
275+ // Incoming call is ringing (not used for outgoing call).
276+ Log .i ("onCallStateChanged" , "CALL_STATE_RINGING" );
277+ break ;
278+ case TelephonyManager .CALL_STATE_OFFHOOK :
279+ // Phone call is active -- off the hook.
280+
281+ // Check if there is active call in native
282+ boolean isInManagedCall = RNCallKeepModule .this .checkIsInManagedCall ();
283+
284+ // Only let the JS side know if there is active app call & active native call
285+ if (RNCallKeepModule .this .hasActiveCall && isInManagedCall ){
286+ WritableMap args = Arguments .createMap ();
287+ RNCallKeepModule .this .sendEventToJS ("RNCallKeepHasActiveCall" ,args );
288+ }else if (VoiceConnectionService .currentConnections .size () > 0 ){
289+ // Will enter here for the first time to mark the app has active call
290+ RNCallKeepModule .this .hasActiveCall = true ;
291+ }
292+ Log .i ("onCallStateChanged" , "CALL_STATE_OFFHOOK" );
293+ break ;
294+ case TelephonyManager .CALL_STATE_IDLE :
295+ // Phone is idle before and after phone call.
296+ // If running on version older than 19 (KitKat),
297+ // restart activity when phone call ends.
298+ Log .i ("onCallStateChanged" , "CALL_STATE_IDLE" );
299+ break ;
300+ default :
301+ Log .i ("onCallStateChanged" , "default" );
302+ break ;
303+ }
304+ }
305+ }
306+
307+ public void stopListenToNativeCallsState () {
308+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && callStateListener !=null ){
309+ Log .d (TAG , "[RNCallKeepModule] stopListenToNativeCallsState" );
310+ telephonyManager .unregisterTelephonyCallback (callStateListener );
311+ }else if (Build .VERSION .SDK_INT < Build .VERSION_CODES .S && legacyCallStateListener != null ){
312+ telephonyManager .listen (legacyCallStateListener , PhoneStateListener .LISTEN_NONE );
313+ }
314+ }
315+
316+ public void listenToNativeCallsState () {
317+ Context context = this .getAppContext ();
318+ int permissionCheck = ContextCompat .checkSelfPermission (context , Manifest .permission .READ_PHONE_STATE );
319+ if (permissionCheck == PackageManager .PERMISSION_GRANTED ) {
320+ Log .d (TAG , "[RNCallKeepModule] listenToNativeCallsState" );
321+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
322+ callStateListener = new CallStateListener ();
323+ telephonyManager .registerTelephonyCallback (context .getMainExecutor (),callStateListener );
324+ } else {
325+ legacyCallStateListener = new LegacyCallStateListener ();
326+ telephonyManager .listen (legacyCallStateListener , PhoneStateListener .LISTEN_CALL_STATE );
327+ }
328+ }
329+ }
330+
331+ public boolean checkIsInManagedCall () {
332+ Context context = this .getAppContext ();
333+ int permissionCheck = ContextCompat .checkSelfPermission (context , Manifest .permission .READ_PHONE_STATE );
334+ if (permissionCheck == PackageManager .PERMISSION_GRANTED ) {
335+ return telecomManager .isInManagedCall ();
336+ }
337+ return false ;
338+ }
339+
340+ @ ReactMethod
341+ public void checkIsInManagedCall (Promise promise ) {
342+ boolean isInManagedCall = this .checkIsInManagedCall ();
343+ promise .resolve (isInManagedCall );
344+ }
345+
220346 @ ReactMethod
221347 public void setSettings (ReadableMap options ) {
222348 Log .d (TAG , "[RNCallKeepModule] setSettings : " + options );
@@ -335,7 +461,7 @@ public void displayIncomingCall(String uuid, String number, String callerName, b
335461 if (payload != null ) {
336462 extras .putBundle (EXTRA_PAYLOAD , payload );
337463 }
338-
464+ this . listenToNativeCallsState ();
339465 telecomManager .addNewIncomingCall (handle , extras );
340466 }
341467
@@ -390,7 +516,7 @@ public void startCall(String uuid, String number, String callerName, boolean has
390516 extras .putParcelable (TelecomManager .EXTRA_OUTGOING_CALL_EXTRAS , callExtras );
391517
392518 Log .d (TAG , "[RNCallKeepModule] startCall, uuid: " + uuid );
393-
519+ this . listenToNativeCallsState ();
394520 telecomManager .placeCall (uri , extras );
395521 }
396522
@@ -411,7 +537,8 @@ public void endCall(String uuid) {
411537 AudioManager audioManager = (AudioManager ) context .getSystemService (context .AUDIO_SERVICE );
412538 audioManager .setMode (0 );
413539 conn .onDisconnect ();
414-
540+ this .stopListenToNativeCallsState ();
541+ this .hasActiveCall = false ;
415542 Log .d (TAG , "[RNCallKeepModule] endCall executed, uuid: " + uuid );
416543 }
417544
@@ -429,7 +556,8 @@ public void endAllCalls() {
429556 Connection connectionToEnd = connectionEntry .getValue ();
430557 connectionToEnd .onDisconnect ();
431558 }
432-
559+ this .stopListenToNativeCallsState ();
560+ this .hasActiveCall = false ;
433561 Log .d (TAG , "[RNCallKeepModule] endAllCalls executed" );
434562 }
435563
@@ -597,6 +725,37 @@ public void reportEndCallWithUUID(String uuid, int reason) {
597725 conn .reportDisconnect (reason );
598726 }
599727
728+ @ Override
729+ public void onHostResume () {
730+ Log .d (TAG , "onResume()" );
731+ }
732+
733+ @ Override
734+ public void onHostPause () {
735+ Log .d (TAG , "onPause()" );
736+ }
737+
738+ @ Override
739+ public void onHostDestroy () {
740+ // when activity destroyed end all calls
741+ Log .d (TAG , "onDestroy()" );
742+ if (!isConnectionServiceAvailable () || !hasPhoneAccount ()) {
743+ Log .w (TAG , "[RNCallKeepModule] endAllCalls ignored due to no ConnectionService or no phone account" );
744+ return ;
745+ }
746+
747+ ArrayList <Map .Entry <String , VoiceConnection >> connections =
748+ new ArrayList <Map .Entry <String , VoiceConnection >>(VoiceConnectionService .currentConnections .entrySet ());
749+ for (Map .Entry <String , VoiceConnection > connectionEntry : connections ) {
750+ Connection connectionToEnd = connectionEntry .getValue ();
751+ connectionToEnd .onDisconnect ();
752+ }
753+ this .stopListenToNativeCallsState ();
754+ Log .d (TAG , "[RNCallKeepModule] endAllCalls executed" );
755+ // this line will kill the android process after ending all calls
756+ android .os .Process .killProcess (android .os .Process .myPid ());
757+ }
758+
600759 @ ReactMethod
601760 public void rejectCall (String uuid ) {
602761 Log .d (TAG , "[RNCallKeepModule] rejectCall, uuid: " + uuid );
@@ -610,7 +769,7 @@ public void rejectCall(String uuid) {
610769 Log .w (TAG , "[RNCallKeepModule] rejectCall ignored because no connection found, uuid: " + uuid );
611770 return ;
612771 }
613-
772+ this . stopListenToNativeCallsState ();
614773 conn .onReject ();
615774 }
616775
0 commit comments