From 845ff8ab7cb74033d643765d75ad579105714599 Mon Sep 17 00:00:00 2001 From: Emmanuel Quentin Date: Thu, 17 Jun 2021 17:09:47 -0400 Subject: [PATCH 1/3] Allow to display incoming call natively on Android --- actions.js | 13 +- .../io/wazo/callkeep/RNCallKeepModule.java | 117 ++++++++++++++---- .../io/wazo/callkeep/RNCallKeepPackage.java | 2 +- 3 files changed, 103 insertions(+), 29 deletions(-) diff --git a/actions.js b/actions.js index 77910b2a..6a57796b 100644 --- a/actions.js +++ b/actions.js @@ -40,8 +40,17 @@ const didActivateAudioSession = handler => const didDeactivateAudioSession = handler => eventEmitter.addListener(RNCallKeepDidDeactivateAudioSession, handler); -const didDisplayIncomingCall = handler => - eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, (data) => handler(data)); +const didDisplayIncomingCall = handler => eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, data => { + // On Android the payload parameter is sent a String + // As it requires too much code on Android to convert it to WritableMap, let's do it here. + if (data.payload && typeof data.payload === 'string') { + try { + data.payload = JSON.parse(data.payload); + } catch (_) { + } + } + handler(data); +}); const didPerformSetMutedCallAction = handler => eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data)); diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index 5b62d33e..fa5c8e5b 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -59,6 +59,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.HeadlessJsTaskService; import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import com.facebook.react.modules.permissions.PermissionsModule; @@ -95,6 +96,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { public static final int REQUEST_READ_PHONE_STATE = 1337; public static final int REQUEST_REGISTER_CALL_PROVIDER = 394859; + public static RNCallKeepModule instance = null; + private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep"; private static String[] permissions = { @@ -112,18 +115,32 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { private boolean isReceiverRegistered = false; private VoiceBroadcastReceiver voiceBroadcastReceiver; private ReadableMap _settings; + private WritableNativeArray delayedEvents; + private boolean hasListeners = false; + + public static RNCallKeepModule getInstance(ReactApplicationContext reactContext, boolean realContext) { + if (instance == null) { + instance = new RNCallKeepModule(reactContext); + } + if (realContext) { + instance.setContext(reactContext); + } + return instance; + } - public RNCallKeepModule(ReactApplicationContext reactContext) { + private RNCallKeepModule(ReactApplicationContext reactContext) { super(reactContext); Log.d(TAG, "[VoiceConnection] constructor"); this.reactContext = reactContext; + delayedEvents = new WritableNativeArray(); + this.registerReceiver(); } private boolean isSelfManaged() { - try { + try { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && _settings.hasKey("selfManaged") && _settings.getBoolean("selfManaged"); - } catch(Exception e) { + } catch (Exception e) { return false; } } @@ -133,6 +150,45 @@ public String getName() { return REACT_NATIVE_MODULE_NAME; } + public void setContext(ReactApplicationContext reactContext) { + Log.d(TAG, "[VoiceConnection] updating react context"); + this.reactContext = reactContext; + } + + public void reportNewIncomingCall(String uuid, String number, String callerName, boolean hasVideo, String payload) { + Log.d(TAG, "[VoiceConnection] reportNewIncomingCall, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName); + // @TODO: handle video + + this.displayIncomingCall(uuid, number, callerName); + + // Send event to JS + WritableMap args = Arguments.createMap(); + args.putString("handle", number); + args.putString("callUUID", uuid); + args.putString("name", callerName); + if (payload != null) { + args.putString("payload", payload); + } + sendEventToJS("RNCallKeepDidDisplayIncomingCall", args); + } + + public void startObserving() { + int count = delayedEvents.size(); + Log.d(TAG, "[VoiceConnection] startObserving, event count: " + count); + if (count > 0) { + this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit("RNCallKeepDidLoadWithEvents", delayedEvents); + } + } + + public void initializeTelecomManager() { + Context context = this.getAppContext(); + ComponentName cName = new ComponentName(context, VoiceConnectionService.class); + String appName = this.getApplicationName(context); + + handle = new PhoneAccountHandle(cName, appName); + telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + } + @ReactMethod public void setup(ReadableMap options) { Log.d(TAG, "[VoiceConnection] setup"); @@ -158,6 +214,7 @@ public void setup(ReadableMap options) { if (isConnectionServiceAvailable()) { this.registerPhoneAccount(options); this.registerEvents(); + this.startObserving(); VoiceConnectionService.setAvailable(true); } @@ -187,8 +244,8 @@ public void registerEvents() { Log.d(TAG, "[VoiceConnection] registerEvents"); - voiceBroadcastReceiver = new VoiceBroadcastReceiver(); - registerReceiver(); + this.hasListeners = true; + this.startObserving(); VoiceConnectionService.setPhoneAccountHandle(handle); } @@ -410,6 +467,11 @@ public void checkDefaultPhoneAccount(Promise promise) { promise.resolve(!hasSim || hasDefaultAccount); } + @ReactMethod + public void getInitialEvents(Promise promise) { + promise.resolve(delayedEvents); + } + @ReactMethod public void setOnHold(String uuid, boolean shouldHold) { Log.d(TAG, "[VoiceConnection] setOnHold, uuid: " + uuid + ", shouldHold: " + (shouldHold ? "true" : "false")); @@ -721,13 +783,21 @@ public void backToForeground() { } } - private void initializeTelecomManager() { - Context context = this.getAppContext(); - ComponentName cName = new ComponentName(context, VoiceConnectionService.class); - String appName = this.getApplicationName(context); + public static void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) { + int permissionsIndex = 0; + List permsList = Arrays.asList(permissions); + for (int result : grantResults) { + if (permsList.contains(grantedPermissions[permissionsIndex]) && result != PackageManager.PERMISSION_GRANTED) { + hasPhoneAccountPromise.resolve(false); + return; + } + permissionsIndex++; + } + hasPhoneAccountPromise.resolve(true); + } - handle = new PhoneAccountHandle(cName, appName); - telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + private boolean isSelfManaged() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && _settings.hasKey("selfManaged") && _settings.getBoolean("selfManaged"); } private void registerPhoneAccount(Context appContext) { @@ -761,8 +831,15 @@ private void registerPhoneAccount(Context appContext) { } private void sendEventToJS(String eventName, @Nullable WritableMap params) { - Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName :" + eventName + ", args : " + (params != null ? params.toString() : "null")); - this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(eventName, params); + boolean isBoundToJS = this.reactContext.hasActiveCatalystInstance(); + Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName :" + eventName + ", bound: " + isBoundToJS + ", hasListeners: " + hasListeners + " args : " + (params != null ? params.toString() : "null")); + + if (isBoundToJS && hasListeners) { + this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(eventName, params); + } else { + params.putString("name", eventName); + delayedEvents.pushMap(params); + } } private String getApplicationName(Context appContext) { @@ -797,6 +874,7 @@ private static boolean hasPhoneAccount() { private void registerReceiver() { if (!isReceiverRegistered) { + voiceBroadcastReceiver = new VoiceBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_END_CALL); intentFilter.addAction(ACTION_ANSWER_CALL); @@ -820,19 +898,6 @@ private Context getAppContext() { return this.reactContext.getApplicationContext(); } - public static void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) { - int permissionsIndex = 0; - List permsList = Arrays.asList(permissions); - for (int result : grantResults) { - if (permsList.contains(grantedPermissions[permissionsIndex]) && result != PackageManager.PERMISSION_GRANTED) { - hasPhoneAccountPromise.resolve(false); - return; - } - permissionsIndex++; - } - hasPhoneAccountPromise.resolve(true); - } - private class VoiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepPackage.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepPackage.java index 6070a75c..640c0f3d 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepPackage.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepPackage.java @@ -30,7 +30,7 @@ public class RNCallKeepPackage implements ReactPackage { @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.singletonList(new RNCallKeepModule(reactContext)); + return Collections.singletonList(RNCallKeepModule.getInstance(reactContext, true)); } // Deprecated RN 0.47 From fb89ad4aa2f3e390f537ea68573a2d47ac0d1bfa Mon Sep 17 00:00:00 2001 From: Emmanuel Quentin Date: Mon, 21 Jun 2021 17:15:02 -0400 Subject: [PATCH 2/3] Fix some native event handling --- .../java/io/wazo/callkeep/RNCallKeepModule.java | 13 ++++++++++++- index.d.ts | 2 ++ index.js | 7 +++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index fa5c8e5b..dda1276c 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -177,6 +177,7 @@ public void startObserving() { Log.d(TAG, "[VoiceConnection] startObserving, event count: " + count); if (count > 0) { this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit("RNCallKeepDidLoadWithEvents", delayedEvents); + delayedEvents = new WritableNativeArray(); } } @@ -249,6 +250,13 @@ public void registerEvents() { VoiceConnectionService.setPhoneAccountHandle(handle); } + @ReactMethod + public void unregisterEvents() { + Log.d(TAG, "[RNCallKeepModule] unregisterEvents"); + + this.hasListeners = false; + } + @ReactMethod public void displayIncomingCall(String uuid, String number, String callerName) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { @@ -832,11 +840,14 @@ private void registerPhoneAccount(Context appContext) { private void sendEventToJS(String eventName, @Nullable WritableMap params) { boolean isBoundToJS = this.reactContext.hasActiveCatalystInstance(); - Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName :" + eventName + ", bound: " + isBoundToJS + ", hasListeners: " + hasListeners + " args : " + (params != null ? params.toString() : "null")); + Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName: " + eventName + ", bound: " + isBoundToJS + ", hasListeners: " + hasListeners + " args : " + (params != null ? params.toString() : "null")); if (isBoundToJS && hasListeners) { this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(eventName, params); } else { + if (params == null) { + params = Arguments.createMap(); + } params.putString("name", eventName); delayedEvents.pushMap(params); } diff --git a/index.d.ts b/index.d.ts index 1259d5c2..ff3b400e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -83,6 +83,8 @@ declare module 'react-native-callkeep' { static registerAndroidEvents(): void + static unregisterAndroidEvents(): void + static displayIncomingCall( uuid: string, handle: string, diff --git a/index.js b/index.js index 1ce9b43c..38f02fed 100644 --- a/index.js +++ b/index.js @@ -62,6 +62,13 @@ class RNCallKeep { RNCallKeepModule.registerEvents(); }; + unregisterAndroidEvents = () => { + if (isIOS) { + return; + } + RNCallKeepModule.unregisterEvents(); + }; + hasDefaultPhoneAccount = async (options) => { if (!isIOS) { return this._hasDefaultPhoneAccount(options); From a778b96f9a54dd7a9468114e2bb768c53125a567 Mon Sep 17 00:00:00 2001 From: Emmanuel Quentin Date: Mon, 5 Jul 2021 16:34:08 -0400 Subject: [PATCH 3/3] Add setSettings method --- .../src/main/java/io/wazo/callkeep/RNCallKeepModule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index dda1276c..7a960088 100644 --- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -190,12 +190,16 @@ public void initializeTelecomManager() { telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); } + public void setSettings(ReadableMap options) { + this._settings = options; + } + @ReactMethod public void setup(ReadableMap options) { Log.d(TAG, "[VoiceConnection] setup"); VoiceConnectionService.setAvailable(false); VoiceConnectionService.setInitialized(true); - this._settings = options; + this.setSettings(options); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (isSelfManaged()) {