diff --git a/README.md b/README.md index 5fcf3b73..407f00ae 100644 --- a/README.md +++ b/README.md @@ -474,6 +474,17 @@ RNCallKeep.addEventListener('didPerformDTMFAction', ({ digits, callUUID }) => { - The digits that emit the dtmf tone - `callUUID` (string) - The UUID of the call. + +### - checkReachability + +On Android when the application is in background, after a certain delay the OS will close every connection with informing about it. +So we have to check if the application is reachable before making a call from the native phone application. + +```js +RNCallKeep.addEventListener('checkReachability', () => { + RNCallKeep.setReachable(); +}); +``` ## Example diff --git a/actions.js b/actions.js index aa542378..2e5ff59e 100644 --- a/actions.js +++ b/actions.js @@ -13,15 +13,16 @@ const RNCallKeepDidPerformSetMutedCallAction = 'RNCallKeepDidPerformSetMutedCall const RNCallKeepDidToggleHoldAction = 'RNCallKeepDidToggleHoldAction'; const RNCallKeepDidPerformDTMFAction = 'RNCallKeepDidPerformDTMFAction'; const RNCallKeepProviderReset = 'RNCallKeepProviderReset'; +const RNCallKeepCheckReachability = 'RNCallKeepCheckReachability'; const isIOS = Platform.OS === 'ios'; const didReceiveStartCallAction = handler => { - eventEmitter.addListener(RNCallKeepDidReceiveStartCallAction, (data) => handler(data)); - if (isIOS) { // Tell CallKeep that we are ready to receive `RNCallKeepDidReceiveStartCallAction` event and prevent delay RNCallKeepModule._startCallActionEventListenerAdded(); } + + return eventEmitter.addListener(RNCallKeepDidReceiveStartCallAction, (data) => handler(data)); }; const answerCall = handler => @@ -51,6 +52,9 @@ const didPerformDTMFAction = handler => const didResetProvider = handler => eventEmitter.addListener(RNCallKeepProviderReset, handler); +const checkReachability = handler => + eventEmitter.addListener(RNCallKeepCheckReachability, handler); + export const listeners = { didReceiveStartCallAction, answerCall, @@ -62,5 +66,5 @@ export const listeners = { didToggleHoldCallAction, didPerformDTMFAction, didResetProvider, + checkReachability, }; - diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index 2d9acf8f..15b9cce2 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -85,6 +85,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { public static final String ACTION_UNHOLD_CALL = "ACTION_UNHOLD_CALL"; public static final String ACTION_ONGOING_CALL = "ACTION_ONGOING_CALL"; public static final String ACTION_AUDIO_SESSION = "ACTION_AUDIO_SESSION"; + public static final String ACTION_CHECK_REACHABILITY = "ACTION_CHECK_REACHABILITY"; private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep"; @@ -360,6 +361,11 @@ public void setAvailable(Boolean active) { VoiceConnectionService.setAvailable(active); } + @ReactMethod + public void setReachable() { + VoiceConnectionService.setReachable(); + } + @ReactMethod public void setCurrentCallActive(String uuid) { Connection conn = VoiceConnectionService.getConnection(uuid); @@ -487,6 +493,7 @@ private void registerReceiver() { intentFilter.addAction(ACTION_HOLD_CALL); intentFilter.addAction(ACTION_ONGOING_CALL); intentFilter.addAction(ACTION_AUDIO_SESSION); + intentFilter.addAction(ACTION_CHECK_REACHABILITY); LocalBroadcastManager.getInstance(this.reactContext).registerReceiver(voiceBroadcastReceiver, intentFilter); isReceiverRegistered = true; } @@ -514,9 +521,6 @@ private class VoiceBroadcastReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { WritableMap args = Arguments.createMap(); HashMap attributeMap = (HashMap)intent.getSerializableExtra("attributeMap"); - if (attributeMap == null) { - return; - } switch (intent.getAction()) { case ACTION_END_CALL: @@ -561,6 +565,9 @@ public void onReceive(Context context, Intent intent) { case ACTION_AUDIO_SESSION: sendEventToJS("RNCallKeepDidActivateAudioSession", null); break; + case ACTION_CHECK_REACHABILITY: + sendEventToJS("RNCallKeepCheckReachability", null); + break; } } } diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java index cce97cef..104a0149 100644 --- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java +++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java @@ -62,6 +62,7 @@ import static io.wazo.callkeep.RNCallKeepModule.ACTION_ONGOING_CALL; import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNHOLD_CALL; import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNMUTE_CALL; +import static io.wazo.callkeep.RNCallKeepModule.ACTION_CHECK_REACHABILITY; import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALLER_NAME; import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_NUMBER; import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_UUID; @@ -70,10 +71,15 @@ // @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionService.java @TargetApi(Build.VERSION_CODES.M) public class VoiceConnectionService extends ConnectionService { - private static Boolean isAvailable = false; + private static Boolean isAvailable; + private static Boolean isInitialized; + private static Boolean isReachable; + private static String notReachableCallUuid; + private static ConnectionRequest currentConnectionRequest; private static String TAG = "RNCK:VoiceConnectionService"; public static Map currentConnections = new HashMap<>(); public static Boolean hasOutgoingCall = false; + public static VoiceConnectionService currentConnectionService = null; public static Connection getConnection(String connectionId) { if (currentConnections.containsKey(connectionId)) { @@ -85,12 +91,27 @@ public static Connection getConnection(String connectionId) { public VoiceConnectionService() { super(); Log.e(TAG, "Constructor"); + isReachable = false; + isInitialized = false; + isAvailable = false; + currentConnectionRequest = null; + currentConnectionService = this; } public static void setAvailable(Boolean value) { + Log.d(TAG, "setAvailable: " + (value ? "true" : "false")); + if (value) { + isInitialized = true; + } + isAvailable = value; } + public static void setReachable() { + Log.d(TAG, "setReachable"); + isReachable = true; + VoiceConnectionService.currentConnectionRequest = null; + } public static void deinitConnection(String connectionId) { Log.d(TAG, "deinitConnection:" + connectionId); @@ -116,44 +137,44 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage @Override public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { VoiceConnectionService.hasOutgoingCall = true; + String uuid = UUID.randomUUID().toString(); + + if (!isInitialized && !isReachable) { + this.notReachableCallUuid = uuid; + this.currentConnectionRequest = request; + this.checkReachability(); + } + + return this.makeOutgoingCall(request, uuid, false); + } + private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) { Bundle extras = request.getExtras(); Connection outgoingCallConnection = null; String number = request.getAddress().getSchemeSpecificPart(); String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); String displayName = extras.getString(EXTRA_CALLER_NAME); - String uuid = UUID.randomUUID().toString(); + Boolean isForeground = VoiceConnectionService.isRunning(this.getApplicationContext()); - Log.d(TAG, "onCreateOutgoingConnection:" + uuid + ", number: " + number); + Log.d(TAG, "makeOutgoingCall:" + uuid + ", number: " + number + ", displayName:" + displayName); // Wakeup application if needed - if (!VoiceConnectionService.isRunning(this.getApplicationContext())) { + if (!isForeground || forceWakeUp) { Log.d(TAG, "onCreateOutgoingConnection: Waking up application"); - Intent headlessIntent = new Intent( - this.getApplicationContext(), - RNCallKeepBackgroundMessagingService.class - ); - headlessIntent.putExtra("callUUID", uuid); - headlessIntent.putExtra("name", displayName); - headlessIntent.putExtra("handle", number); - ComponentName name = this.getApplicationContext().startService(headlessIntent); - if (name != null) { - HeadlessJsTaskService.acquireWakeLockNow(this.getApplicationContext()); - } - } else if (!this.canMakeOutgoingCall()) { + this.wakeUpApplication(uuid, number, displayName); + } else if (!this.canMakeOutgoingCall() && isReachable) { + Log.d(TAG, "onCreateOutgoingConnection: not available"); return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.LOCAL)); } // TODO: Hold all other calls - if (extrasNumber != null && extrasNumber.equals(number)) { - outgoingCallConnection = createConnection(request); - } else { + if (extrasNumber == null || !extrasNumber.equals(number)) { extras.putString(EXTRA_CALL_UUID, uuid); extras.putString(EXTRA_CALLER_NAME, displayName); extras.putString(EXTRA_CALL_NUMBER, number); - outgoingCallConnection = createConnection(request); } + outgoingCallConnection = createConnection(request); outgoingCallConnection.setDialing(); outgoingCallConnection.setAudioModeIsVoip(true); outgoingCallConnection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED); @@ -164,15 +185,59 @@ public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManage sendCallRequestToActivity(ACTION_ONGOING_CALL, extrasMap); sendCallRequestToActivity(ACTION_AUDIO_SESSION, null); + Log.d(TAG, "onCreateOutgoingConnection: calling"); + return outgoingCallConnection; } + private void wakeUpApplication(String uuid, String number, String displayName) { + Intent headlessIntent = new Intent( + this.getApplicationContext(), + RNCallKeepBackgroundMessagingService.class + ); + headlessIntent.putExtra("callUUID", uuid); + headlessIntent.putExtra("name", displayName); + headlessIntent.putExtra("handle", number); + Log.d(TAG, "wakeUpApplication: " + uuid + ", number : " + number + ", displayName:" + displayName); + + ComponentName name = this.getApplicationContext().startService(headlessIntent); + if (name != null) { + HeadlessJsTaskService.acquireWakeLockNow(this.getApplicationContext()); + } + } + + private void wakeUpAfterReachabilityTimeout(ConnectionRequest request) { + if (this.currentConnectionRequest == null) { + return; + } + Log.d(TAG, "checkReachability timeout, force wakeup"); + Bundle extras = request.getExtras(); + String number = request.getAddress().getSchemeSpecificPart(); + String displayName = extras.getString(EXTRA_CALLER_NAME); + wakeUpApplication(this.notReachableCallUuid, number, displayName); + + VoiceConnectionService.currentConnectionRequest = null; + } + + private void checkReachability() { + Log.d(TAG, "checkReachability"); + + final VoiceConnectionService instance = this; + sendCallRequestToActivity(ACTION_CHECK_REACHABILITY, null); + + new android.os.Handler().postDelayed( + new Runnable() { + public void run() { + instance.wakeUpAfterReachabilityTimeout(instance.currentConnectionRequest); + } + }, 2000); + } + private Boolean canMakeOutgoingCall() { return isAvailable; } private Connection createConnection(ConnectionRequest request) { - Bundle extras = request.getExtras(); HashMap extrasMap = this.bundleToMap(extras); extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString()); diff --git a/index.d.ts b/index.d.ts index 7de793ae..65a19e94 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,7 @@ export type Events = 'didToggleHoldCallAction' | 'didPerformDTMFAction' | 'didResetProvider' | + 'checkReachability' | 'didPerformSetMutedCallAction'; type HandleType = 'generic' | 'number' | 'email'; @@ -110,6 +111,10 @@ export default class RNCallKeep { } + static setReachable() { + + } + /** * @description supportConnectionService method is available only on Android. */ diff --git a/index.js b/index.js index 4408a467..f4613c2d 100644 --- a/index.js +++ b/index.js @@ -9,24 +9,23 @@ const supportConnectionService = !isIOS && Platform.Version >= 23; class RNCallKeep { constructor() { - this._callkitEventHandlers = new Map(); + this._callkeepEventHandlers = new Map(); } - addEventListener = (type, handler) => { const listener = listeners[type](handler); - this._callkitEventHandlers.set(handler, listener); + this._callkeepEventHandlers.set(type, listener); }; - removeEventListener = (type, handler) => { - const listener = this._callkitEventHandlers.get(handler); + removeEventListener = (type) => { + const listener = this._callkeepEventHandlers.get(type); if (!listener) { return; } listener.remove(); - this._callkitEventHandlers.delete(handler); + this._callkeepEventHandlers.delete(type); }; setup = async (options) => { @@ -83,8 +82,7 @@ class RNCallKeep { } }; - reportEndCallWithUUID = (uuid, reason) => - RNCallKeepModule.reportEndCallWithUUID(uuid, reason); + reportEndCallWithUUID = (uuid, reason) => RNCallKeepModule.reportEndCallWithUUID(uuid, reason); /* * Android explicitly states we reject a call @@ -147,9 +145,11 @@ class RNCallKeep { setOnHold = (uuid, shouldHold) => RNCallKeepModule.setOnHold(uuid, shouldHold); + setReachable = () => RNCallKeepModule.setReachable(); + // @deprecated reportUpdatedCall = (uuid, localizedCallerName) => { - console.warn('RNCallKeep.reportUpdatedCall is deprecarted, use RNCallKeep.updateDisplay instead'); + console.warn('RNCallKeep.reportUpdatedCall is deprecated, use RNCallKeep.updateDisplay instead'); return isIOS ? RNCallKeepModule.reportUpdatedCall(uuid, localizedCallerName) diff --git a/ios/RNCallKeep/RNCallKeep.m b/ios/RNCallKeep/RNCallKeep.m index d40eef9b..83d2ffdc 100644 --- a/ios/RNCallKeep/RNCallKeep.m +++ b/ios/RNCallKeep/RNCallKeep.m @@ -32,6 +32,7 @@ static NSString *const RNCallKeepPerformPlayDTMFCallAction = @"RNCallKeepDidPerformDTMFAction"; static NSString *const RNCallKeepDidToggleHoldAction = @"RNCallKeepDidToggleHoldAction"; static NSString *const RNCallKeepProviderReset = @"RNCallKeepProviderReset"; +static NSString *const RNCallKeepCheckReachability = @"RNCallKeepCheckReachability"; @implementation RNCallKeep { @@ -90,7 +91,8 @@ - (void)dealloc RNCallKeepDidPerformSetMutedCallAction, RNCallKeepPerformPlayDTMFCallAction, RNCallKeepDidToggleHoldAction, - RNCallKeepProviderReset + RNCallKeepProviderReset, + RNCallKeepCheckReachability ]; } diff --git a/package.json b/package.json index d854f7fd..ec9b8be5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-callkeep", - "version": "3.0.1", + "version": "3.0.2", "description": "iOS 10 CallKit and Android ConnectionService Framework For React Native", "main": "index.js", "scripts": {},