diff --git a/README.md b/README.md index 1388a153..be61547f 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ Self Managed calling apps are an advanced topic, and there are many steps involv | [setForegroundServiceSettings()](#setForegroundServiceSettings) | `Promise` | ❌ | ✅ | | [canMakeMultipleCalls()](#canMakeMultipleCalls) | `Promise` | ❌ | ✅ | | [setCurrentCallActive()](#setCurrentCallActive) | `Promise` | ❌ | ✅ | +| [checkIsInManagedCall()](#setAvailable) | `Promise` | ❌ | ✅ | | [isCallActive()](#isCallActive) | `Promise` | ✅ | ❌ | | [getCalls()](#getCalls) | `Promise` | ✅ | ❌ | | [displayIncomingCall()](#displayIncomingCall) | `Promise` | ✅ | ✅ | @@ -316,6 +317,16 @@ RNCallKeep.setCurrentCallActive(uuid); - `uuid`: string - The `uuid` used for `startCall` or `displayIncomingCall` +### checkIsInManagedCall +_This feature is available only on Android._ + +Returns true if there is an active native call + +```js +RNCallKeep.checkIsInManagedCall(); +``` + + ### isCallActive _This feature is available only on IOS._ @@ -741,6 +752,7 @@ RNCallKeep.registerAndroidEvents(); | [silenceIncomingCall](#silenceIncomingCall) | ❌ | ✅ | | [checkReachability](#checkReachability) | ❌ | ✅ | | [didChangeAudioRoute](#didChangeAudioRoute) | ✅ | ✅ | +| [onHasActiveCall](#onHasActiveCall) | ❌ | ✅ | ### didReceiveStartCallAction @@ -993,6 +1005,19 @@ RNCallKeep.addEventListener('checkReachability', () => { ``` +### onHasActiveCall + +_Android only._ + +A listener that tells the JS side if a native call has been answered while there was an active self-managed call + +```js +RNCallKeep.addEventListener('onHasActiveCall', () => { + // eg: End active app call if native call is answered +}); + +``` + ## Example A full example is available in the [example](https://github.com/react-native-webrtc/react-native-callkeep/tree/master/example) folder. diff --git a/actions.js b/actions.js index 4e936360..fbbe4ca2 100644 --- a/actions.js +++ b/actions.js @@ -19,6 +19,7 @@ const RNCallKeepShowIncomingCallUi = 'RNCallKeepShowIncomingCallUi'; const RNCallKeepOnSilenceIncomingCall = 'RNCallKeepOnSilenceIncomingCall'; const RNCallKeepOnIncomingConnectionFailed = 'RNCallKeepOnIncomingConnectionFailed'; const RNCallKeepDidChangeAudioRoute = 'RNCallKeepDidChangeAudioRoute'; +const RNCallKeepHasActiveCall = 'RNCallKeepHasActiveCall'; const isIOS = Platform.OS === 'ios'; const didReceiveStartCallAction = handler => { @@ -60,6 +61,9 @@ const didDisplayIncomingCall = handler => eventEmitter.addListener(RNCallKeepDid const didPerformSetMutedCallAction = handler => eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data)); +const onHasActiveCall = handler => + eventEmitter.addListener(RNCallKeepHasActiveCall, handler); + const didToggleHoldCallAction = handler => eventEmitter.addListener(RNCallKeepDidToggleHoldAction, handler); @@ -103,4 +107,5 @@ export const listeners = { silenceIncomingCall, createIncomingConnectionFailed, didChangeAudioRoute, + onHasActiveCall }; diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index 19d43c69..b89954b8 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -16,7 +16,7 @@ */ package io.wazo.callkeep; - +import com.facebook.react.bridge.LifecycleEventListener; import android.Manifest; import android.app.Activity; import android.content.BroadcastReceiver; @@ -48,6 +48,8 @@ import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.TelephonyManager; +import android.telephony.TelephonyCallback; +import android.telephony.PhoneStateListener; import android.util.Log; import com.facebook.react.bridge.Arguments; @@ -101,7 +103,7 @@ import static io.wazo.callkeep.Constants.ACTION_DID_CHANGE_AUDIO_ROUTE; // @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java -public class RNCallKeepModule extends ReactContextBaseJavaModule { +public class RNCallKeepModule extends ReactContextBaseJavaModule implements LifecycleEventListener { public static final int REQUEST_READ_PHONE_STATE = 1337; public static final int REQUEST_REGISTER_CALL_PROVIDER = 394859; @@ -117,6 +119,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { private static final String TAG = "RNCallKeep"; private static TelecomManager telecomManager; + private LegacyCallStateListener legacyCallStateListener; + private CallStateListener callStateListener; private static TelephonyManager telephonyManager; private static Promise hasPhoneAccountPromise; private ReactApplicationContext reactContext; @@ -126,6 +130,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { private static WritableMap _settings; private WritableNativeArray delayedEvents; private boolean hasListeners = false; + private boolean hasActiveCall = false; public static RNCallKeepModule getInstance(ReactApplicationContext reactContext, boolean realContext) { if (instance == null) { @@ -150,6 +155,8 @@ public static WritableMap getSettings(@Nullable Context context) { private RNCallKeepModule(ReactApplicationContext reactContext) { super(reactContext); + // This line for listening to the Activity Lifecycle Events so we can end the calls onDestroy + reactContext.addLifecycleEventListener(this); Log.d(TAG, "[RNCallKeepModule] constructor"); this.reactContext = reactContext; @@ -217,6 +224,120 @@ public void initializeTelecomManager() { telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); } + + + /** + * Monitors and logs phone call activities, and shows the phone state + */ + private class LegacyCallStateListener extends PhoneStateListener { + + @Override + public void onCallStateChanged(int state, String incomingNumber) { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + // Incoming call is ringing (not used for outgoing call). + break; + case TelephonyManager.CALL_STATE_OFFHOOK: + // Phone call is active -- off the hook. + // Check if there is active call in native + boolean isInManagedCall = RNCallKeepModule.this.checkIsInManagedCall(); + + // Only let the JS side know if there is active app call & active native call + if(RNCallKeepModule.this.hasActiveCall && isInManagedCall){ + WritableMap args = Arguments.createMap(); + RNCallKeepModule.this.sendEventToJS("RNCallKeepHasActiveCall",args); + }else if(VoiceConnectionService.currentConnections.size() > 0){ + // Will enter here for the first time to mark the app has active call + RNCallKeepModule.this.hasActiveCall = true; + } + break; + case TelephonyManager.CALL_STATE_IDLE: + // Phone is idle before and after phone call. + // If running on version older than 19 (KitKat), + // restart activity when phone call ends. + break; + default: + break; + } + } + } + + private class CallStateListener extends TelephonyCallback implements TelephonyCallback.CallStateListener { + + @Override + public void onCallStateChanged(int state) { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + // Incoming call is ringing (not used for outgoing call). + break; + case TelephonyManager.CALL_STATE_OFFHOOK: + // Phone call is active -- off the hook. + + // Check if there is active call in native + boolean isInManagedCall = RNCallKeepModule.this.checkIsInManagedCall(); + + // Only let the JS side know if there is active app call & active native call + if(RNCallKeepModule.this.hasActiveCall && isInManagedCall){ + WritableMap args = Arguments.createMap(); + RNCallKeepModule.this.sendEventToJS("RNCallKeepHasActiveCall",args); + }else if(VoiceConnectionService.currentConnections.size() > 0){ + // Will enter here for the first time to mark the app has active call + RNCallKeepModule.this.hasActiveCall = true; + } + break; + case TelephonyManager.CALL_STATE_IDLE: + // Phone is idle before and after phone call. + // If running on version older than 19 (KitKat), + // restart activity when phone call ends. + break; + default: + break; + } + } + } + + public void stopListenToNativeCallsState() { + Log.d(TAG, "[RNCallKeepModule] stopListenToNativeCallsState"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callStateListener !=null){ + telephonyManager.unregisterTelephonyCallback(callStateListener); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && legacyCallStateListener != null){ + telephonyManager.listen(legacyCallStateListener, PhoneStateListener.LISTEN_NONE); + } + } + + public void listenToNativeCallsState() { + Log.d(TAG, "[RNCallKeepModule] listenToNativeCallsState"); + Context context = this.getAppContext(); + int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE); + + if (permissionCheck == PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callStateListener = new CallStateListener(); + telephonyManager.registerTelephonyCallback(context.getMainExecutor(),callStateListener); + } else { + legacyCallStateListener = new LegacyCallStateListener(); + telephonyManager.listen(legacyCallStateListener, PhoneStateListener.LISTEN_CALL_STATE); + } + } + } + + public boolean checkIsInManagedCall() { + Context context = this.getAppContext(); + int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE); + + if (permissionCheck == PackageManager.PERMISSION_GRANTED) { + return telecomManager.isInManagedCall(); + } + return false; + } + + @ReactMethod + public void checkIsInManagedCall(Promise promise) { + boolean isInManagedCall = this.checkIsInManagedCall(); + promise.resolve(isInManagedCall); + } + @ReactMethod public void setSettings(ReadableMap options) { Log.d(TAG, "[RNCallKeepModule] setSettings : " + options); @@ -335,7 +456,7 @@ public void displayIncomingCall(String uuid, String number, String callerName, b if (payload != null) { extras.putBundle(EXTRA_PAYLOAD, payload); } - + this.listenToNativeCallsState(); telecomManager.addNewIncomingCall(handle, extras); } @@ -390,7 +511,7 @@ public void startCall(String uuid, String number, String callerName, boolean has extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras); Log.d(TAG, "[RNCallKeepModule] startCall, uuid: " + uuid); - + this.listenToNativeCallsState(); telecomManager.placeCall(uri, extras); } @@ -411,7 +532,8 @@ public void endCall(String uuid) { AudioManager audioManager = (AudioManager) context.getSystemService(context.AUDIO_SERVICE); audioManager.setMode(0); conn.onDisconnect(); - + this.stopListenToNativeCallsState(); + this.hasActiveCall = false; Log.d(TAG, "[RNCallKeepModule] endCall executed, uuid: " + uuid); } @@ -429,7 +551,8 @@ public void endAllCalls() { Connection connectionToEnd = connectionEntry.getValue(); connectionToEnd.onDisconnect(); } - + this.stopListenToNativeCallsState(); + this.hasActiveCall = false; Log.d(TAG, "[RNCallKeepModule] endAllCalls executed"); } @@ -597,6 +720,37 @@ public void reportEndCallWithUUID(String uuid, int reason) { conn.reportDisconnect(reason); } + @Override + public void onHostResume() { + + } + + @Override + public void onHostPause() { + + } + + @Override + public void onHostDestroy() { + // When activity destroyed end all calls + Log.d(TAG, "[RNCallKeepModule] onHostDestroy called"); + if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { + Log.w(TAG, "[RNCallKeepModule] onHostDestroy ignored due to no ConnectionService or no phone account"); + return; + } + + ArrayList> connections = + new ArrayList>(VoiceConnectionService.currentConnections.entrySet()); + for (Map.Entry connectionEntry : connections) { + Connection connectionToEnd = connectionEntry.getValue(); + connectionToEnd.onDisconnect(); + } + this.stopListenToNativeCallsState(); + Log.d(TAG, "[RNCallKeepModule] onHostDestroy executed"); + // This line will kill the android process after ending all calls + android.os.Process.killProcess(android.os.Process.myPid()); + } + @ReactMethod public void rejectCall(String uuid) { Log.d(TAG, "[RNCallKeepModule] rejectCall, uuid: " + uuid); @@ -610,7 +764,7 @@ public void rejectCall(String uuid) { Log.w(TAG, "[RNCallKeepModule] rejectCall ignored because no connection found, uuid: " + uuid); return; } - + this.stopListenToNativeCallsState(); conn.onReject(); } diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java index f53c0521..78f359d3 100644 --- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java +++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java @@ -218,17 +218,23 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage @Override public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { VoiceConnectionService.hasOutgoingCall = true; - String uuid = UUID.randomUUID().toString(); - Log.d(TAG, "[VoiceConnectionService] onCreateOutgoingConnection, uuid:" + uuid); + Bundle extras = request.getExtras(); + String callUUID = extras.getString(EXTRA_CALL_UUID); + + if(callUUID == null || callUUID == ""){ + callUUID = UUID.randomUUID().toString(); + } + + Log.d(TAG, "[VoiceConnectionService] onCreateOutgoingConnection, uuid:" + callUUID); if (!isInitialized && !isReachable) { - this.notReachableCallUuid = uuid; + this.notReachableCallUuid = callUUID; this.currentConnectionRequest = request; this.checkReachability(); } - return this.makeOutgoingCall(request, uuid, false); + return this.makeOutgoingCall(request, callUUID, false); } private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) { diff --git a/index.d.ts b/index.d.ts index 9ef03829..25b43da9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,6 +16,7 @@ declare module 'react-native-callkeep' { checkReachability: 'RNCallKeepCheckReachability'; didResetProvider: 'RNCallKeepProviderReset'; didLoadWithEvents: 'RNCallKeepDidLoadWithEvents'; + onHasActiveCall : 'onHasActiveCall'; } export type InitialEvents = Array<{ @@ -54,6 +55,7 @@ declare module 'react-native-callkeep' { checkReachability: undefined; didResetProvider: undefined; didLoadWithEvents: InitialEvents; + onHasActiveCall : undefined; } type HandleType = 'generic' | 'number' | 'email'; @@ -74,7 +76,7 @@ declare module 'react-native-callkeep' { defaultToSpeaker = 0x8, overrideMutedMicrophoneInterruption = 0x80, } - + export enum AudioSessionMode { default = 'AVAudioSessionModeDefault', gameChat = 'AVAudioSessionModeGameChat', @@ -273,5 +275,10 @@ declare module 'react-native-callkeep' { static setCurrentCallActive(callUUID: string): void static backToForeground(): void + + /** + * @descriptions Android Only, Check if there is active native call + */ + static checkIsInManagedCall(): Promise } } diff --git a/index.js b/index.js index 7d31715d..a011f677 100644 --- a/index.js +++ b/index.js @@ -161,6 +161,8 @@ class RNCallKeep { ); }; + checkIsInManagedCall = async () => isIOS? false: RNCallKeepModule.checkIsInManagedCall(); + answerIncomingCall = (uuid) => { RNCallKeepModule.answerIncomingCall(uuid); };