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,120 @@ 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+ break ;
240+ case TelephonyManager .CALL_STATE_OFFHOOK :
241+ // Phone call is active -- off the hook.
242+ // Check if there is active call in native
243+ boolean isInManagedCall = RNCallKeepModule .this .checkIsInManagedCall ();
244+
245+ // Only let the JS side know if there is active app call & active native call
246+ if (RNCallKeepModule .this .hasActiveCall && isInManagedCall ){
247+ WritableMap args = Arguments .createMap ();
248+ RNCallKeepModule .this .sendEventToJS ("RNCallKeepHasActiveCall" ,args );
249+ }else if (VoiceConnectionService .currentConnections .size () > 0 ){
250+ // Will enter here for the first time to mark the app has active call
251+ RNCallKeepModule .this .hasActiveCall = true ;
252+ }
253+ break ;
254+ case TelephonyManager .CALL_STATE_IDLE :
255+ // Phone is idle before and after phone call.
256+ // If running on version older than 19 (KitKat),
257+ // restart activity when phone call ends.
258+ break ;
259+ default :
260+ break ;
261+ }
262+ }
263+ }
264+
265+ private class CallStateListener extends TelephonyCallback implements TelephonyCallback .CallStateListener {
266+
267+ @ Override
268+ public void onCallStateChanged (int state ) {
269+ switch (state ) {
270+ case TelephonyManager .CALL_STATE_RINGING :
271+ // Incoming call is ringing (not used for outgoing call).
272+ break ;
273+ case TelephonyManager .CALL_STATE_OFFHOOK :
274+ // Phone call is active -- off the hook.
275+
276+ // Check if there is active call in native
277+ boolean isInManagedCall = RNCallKeepModule .this .checkIsInManagedCall ();
278+
279+ // Only let the JS side know if there is active app call & active native call
280+ if (RNCallKeepModule .this .hasActiveCall && isInManagedCall ){
281+ WritableMap args = Arguments .createMap ();
282+ RNCallKeepModule .this .sendEventToJS ("RNCallKeepHasActiveCall" ,args );
283+ }else if (VoiceConnectionService .currentConnections .size () > 0 ){
284+ // Will enter here for the first time to mark the app has active call
285+ RNCallKeepModule .this .hasActiveCall = true ;
286+ }
287+ break ;
288+ case TelephonyManager .CALL_STATE_IDLE :
289+ // Phone is idle before and after phone call.
290+ // If running on version older than 19 (KitKat),
291+ // restart activity when phone call ends.
292+ break ;
293+ default :
294+ break ;
295+ }
296+ }
297+ }
298+
299+ public void stopListenToNativeCallsState () {
300+ Log .d (TAG , "[RNCallKeepModule] stopListenToNativeCallsState" );
301+
302+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && callStateListener !=null ){
303+ telephonyManager .unregisterTelephonyCallback (callStateListener );
304+ } else if (Build .VERSION .SDK_INT < Build .VERSION_CODES .S && legacyCallStateListener != null ){
305+ telephonyManager .listen (legacyCallStateListener , PhoneStateListener .LISTEN_NONE );
306+ }
307+ }
308+
309+ public void listenToNativeCallsState () {
310+ Log .d (TAG , "[RNCallKeepModule] listenToNativeCallsState" );
311+ Context context = this .getAppContext ();
312+ int permissionCheck = ContextCompat .checkSelfPermission (context , Manifest .permission .READ_PHONE_STATE );
313+
314+ if (permissionCheck == PackageManager .PERMISSION_GRANTED ) {
315+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
316+ callStateListener = new CallStateListener ();
317+ telephonyManager .registerTelephonyCallback (context .getMainExecutor (),callStateListener );
318+ } else {
319+ legacyCallStateListener = new LegacyCallStateListener ();
320+ telephonyManager .listen (legacyCallStateListener , PhoneStateListener .LISTEN_CALL_STATE );
321+ }
322+ }
323+ }
324+
325+ public boolean checkIsInManagedCall () {
326+ Context context = this .getAppContext ();
327+ int permissionCheck = ContextCompat .checkSelfPermission (context , Manifest .permission .READ_PHONE_STATE );
328+
329+ if (permissionCheck == PackageManager .PERMISSION_GRANTED ) {
330+ return telecomManager .isInManagedCall ();
331+ }
332+ return false ;
333+ }
334+
335+ @ ReactMethod
336+ public void checkIsInManagedCall (Promise promise ) {
337+ boolean isInManagedCall = this .checkIsInManagedCall ();
338+ promise .resolve (isInManagedCall );
339+ }
340+
220341 @ ReactMethod
221342 public void setSettings (ReadableMap options ) {
222343 Log .d (TAG , "[RNCallKeepModule] setSettings : " + options );
@@ -335,7 +456,7 @@ public void displayIncomingCall(String uuid, String number, String callerName, b
335456 if (payload != null ) {
336457 extras .putBundle (EXTRA_PAYLOAD , payload );
337458 }
338-
459+ this . listenToNativeCallsState ();
339460 telecomManager .addNewIncomingCall (handle , extras );
340461 }
341462
@@ -390,7 +511,7 @@ public void startCall(String uuid, String number, String callerName, boolean has
390511 extras .putParcelable (TelecomManager .EXTRA_OUTGOING_CALL_EXTRAS , callExtras );
391512
392513 Log .d (TAG , "[RNCallKeepModule] startCall, uuid: " + uuid );
393-
514+ this . listenToNativeCallsState ();
394515 telecomManager .placeCall (uri , extras );
395516 }
396517
@@ -411,7 +532,8 @@ public void endCall(String uuid) {
411532 AudioManager audioManager = (AudioManager ) context .getSystemService (context .AUDIO_SERVICE );
412533 audioManager .setMode (0 );
413534 conn .onDisconnect ();
414-
535+ this .stopListenToNativeCallsState ();
536+ this .hasActiveCall = false ;
415537 Log .d (TAG , "[RNCallKeepModule] endCall executed, uuid: " + uuid );
416538 }
417539
@@ -429,7 +551,8 @@ public void endAllCalls() {
429551 Connection connectionToEnd = connectionEntry .getValue ();
430552 connectionToEnd .onDisconnect ();
431553 }
432-
554+ this .stopListenToNativeCallsState ();
555+ this .hasActiveCall = false ;
433556 Log .d (TAG , "[RNCallKeepModule] endAllCalls executed" );
434557 }
435558
@@ -597,6 +720,37 @@ public void reportEndCallWithUUID(String uuid, int reason) {
597720 conn .reportDisconnect (reason );
598721 }
599722
723+ @ Override
724+ public void onHostResume () {
725+
726+ }
727+
728+ @ Override
729+ public void onHostPause () {
730+
731+ }
732+
733+ @ Override
734+ public void onHostDestroy () {
735+ // When activity destroyed end all calls
736+ Log .d (TAG , "[RNCallKeepModule] onHostDestroy called" );
737+ if (!isConnectionServiceAvailable () || !hasPhoneAccount ()) {
738+ Log .w (TAG , "[RNCallKeepModule] onHostDestroy ignored due to no ConnectionService or no phone account" );
739+ return ;
740+ }
741+
742+ ArrayList <Map .Entry <String , VoiceConnection >> connections =
743+ new ArrayList <Map .Entry <String , VoiceConnection >>(VoiceConnectionService .currentConnections .entrySet ());
744+ for (Map .Entry <String , VoiceConnection > connectionEntry : connections ) {
745+ Connection connectionToEnd = connectionEntry .getValue ();
746+ connectionToEnd .onDisconnect ();
747+ }
748+ this .stopListenToNativeCallsState ();
749+ Log .d (TAG , "[RNCallKeepModule] onHostDestroy executed" );
750+ // This line will kill the android process after ending all calls
751+ android .os .Process .killProcess (android .os .Process .myPid ());
752+ }
753+
600754 @ ReactMethod
601755 public void rejectCall (String uuid ) {
602756 Log .d (TAG , "[RNCallKeepModule] rejectCall, uuid: " + uuid );
@@ -610,7 +764,7 @@ public void rejectCall(String uuid) {
610764 Log .w (TAG , "[RNCallKeepModule] rejectCall ignored because no connection found, uuid: " + uuid );
611765 return ;
612766 }
613-
767+ this . stopListenToNativeCallsState ();
614768 conn .onReject ();
615769 }
616770
0 commit comments