Skip to content

Commit 559933a

Browse files
authored
Merge pull request #440 from react-native-webrtc/display_incoming_call_natively_android
Allow to display incoming call natively on Android
2 parents 0b891fd + a778b96 commit 559933a

File tree

5 files changed

+128
-30
lines changed

5 files changed

+128
-30
lines changed

actions.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ const didActivateAudioSession = handler =>
4040
const didDeactivateAudioSession = handler =>
4141
eventEmitter.addListener(RNCallKeepDidDeactivateAudioSession, handler);
4242

43-
const didDisplayIncomingCall = handler =>
44-
eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, (data) => handler(data));
43+
const didDisplayIncomingCall = handler => eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, data => {
44+
// On Android the payload parameter is sent a String
45+
// As it requires too much code on Android to convert it to WritableMap, let's do it here.
46+
if (data.payload && typeof data.payload === 'string') {
47+
try {
48+
data.payload = JSON.parse(data.payload);
49+
} catch (_) {
50+
}
51+
}
52+
handler(data);
53+
});
4554

4655
const didPerformSetMutedCallAction = handler =>
4756
eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data));

android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import com.facebook.react.bridge.ReadableMap;
6060
import com.facebook.react.bridge.WritableArray;
6161
import com.facebook.react.bridge.WritableMap;
62+
import com.facebook.react.bridge.WritableNativeArray;
6263
import com.facebook.react.HeadlessJsTaskService;
6364
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
6465
import com.facebook.react.modules.permissions.PermissionsModule;
@@ -95,6 +96,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
9596
public static final int REQUEST_READ_PHONE_STATE = 1337;
9697
public static final int REQUEST_REGISTER_CALL_PROVIDER = 394859;
9798

99+
public static RNCallKeepModule instance = null;
100+
98101
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
99102
private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep";
100103
private static String[] permissions = {
@@ -112,18 +115,32 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
112115
private boolean isReceiverRegistered = false;
113116
private VoiceBroadcastReceiver voiceBroadcastReceiver;
114117
private ReadableMap _settings;
118+
private WritableNativeArray delayedEvents;
119+
private boolean hasListeners = false;
120+
121+
public static RNCallKeepModule getInstance(ReactApplicationContext reactContext, boolean realContext) {
122+
if (instance == null) {
123+
instance = new RNCallKeepModule(reactContext);
124+
}
125+
if (realContext) {
126+
instance.setContext(reactContext);
127+
}
128+
return instance;
129+
}
115130

116-
public RNCallKeepModule(ReactApplicationContext reactContext) {
131+
private RNCallKeepModule(ReactApplicationContext reactContext) {
117132
super(reactContext);
118133
Log.d(TAG, "[VoiceConnection] constructor");
119134

120135
this.reactContext = reactContext;
136+
delayedEvents = new WritableNativeArray();
137+
this.registerReceiver();
121138
}
122139

123140
private boolean isSelfManaged() {
124-
try {
141+
try {
125142
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && _settings.hasKey("selfManaged") && _settings.getBoolean("selfManaged");
126-
} catch(Exception e) {
143+
} catch (Exception e) {
127144
return false;
128145
}
129146
}
@@ -133,12 +150,56 @@ public String getName() {
133150
return REACT_NATIVE_MODULE_NAME;
134151
}
135152

153+
public void setContext(ReactApplicationContext reactContext) {
154+
Log.d(TAG, "[VoiceConnection] updating react context");
155+
this.reactContext = reactContext;
156+
}
157+
158+
public void reportNewIncomingCall(String uuid, String number, String callerName, boolean hasVideo, String payload) {
159+
Log.d(TAG, "[VoiceConnection] reportNewIncomingCall, uuid: " + uuid + ", number: " + number + ", callerName: " + callerName);
160+
// @TODO: handle video
161+
162+
this.displayIncomingCall(uuid, number, callerName);
163+
164+
// Send event to JS
165+
WritableMap args = Arguments.createMap();
166+
args.putString("handle", number);
167+
args.putString("callUUID", uuid);
168+
args.putString("name", callerName);
169+
if (payload != null) {
170+
args.putString("payload", payload);
171+
}
172+
sendEventToJS("RNCallKeepDidDisplayIncomingCall", args);
173+
}
174+
175+
public void startObserving() {
176+
int count = delayedEvents.size();
177+
Log.d(TAG, "[VoiceConnection] startObserving, event count: " + count);
178+
if (count > 0) {
179+
this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit("RNCallKeepDidLoadWithEvents", delayedEvents);
180+
delayedEvents = new WritableNativeArray();
181+
}
182+
}
183+
184+
public void initializeTelecomManager() {
185+
Context context = this.getAppContext();
186+
ComponentName cName = new ComponentName(context, VoiceConnectionService.class);
187+
String appName = this.getApplicationName(context);
188+
189+
handle = new PhoneAccountHandle(cName, appName);
190+
telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
191+
}
192+
193+
public void setSettings(ReadableMap options) {
194+
this._settings = options;
195+
}
196+
136197
@ReactMethod
137198
public void setup(ReadableMap options) {
138199
Log.d(TAG, "[VoiceConnection] setup");
139200
VoiceConnectionService.setAvailable(false);
140201
VoiceConnectionService.setInitialized(true);
141-
this._settings = options;
202+
this.setSettings(options);
142203

143204
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
144205
if (isSelfManaged()) {
@@ -158,6 +219,7 @@ public void setup(ReadableMap options) {
158219
if (isConnectionServiceAvailable()) {
159220
this.registerPhoneAccount(options);
160221
this.registerEvents();
222+
this.startObserving();
161223
VoiceConnectionService.setAvailable(true);
162224
}
163225

@@ -187,11 +249,18 @@ public void registerEvents() {
187249

188250
Log.d(TAG, "[VoiceConnection] registerEvents");
189251

190-
voiceBroadcastReceiver = new VoiceBroadcastReceiver();
191-
registerReceiver();
252+
this.hasListeners = true;
253+
this.startObserving();
192254
VoiceConnectionService.setPhoneAccountHandle(handle);
193255
}
194256

257+
@ReactMethod
258+
public void unregisterEvents() {
259+
Log.d(TAG, "[RNCallKeepModule] unregisterEvents");
260+
261+
this.hasListeners = false;
262+
}
263+
195264
@ReactMethod
196265
public void displayIncomingCall(String uuid, String number, String callerName) {
197266
if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
@@ -410,6 +479,11 @@ public void checkDefaultPhoneAccount(Promise promise) {
410479
promise.resolve(!hasSim || hasDefaultAccount);
411480
}
412481

482+
@ReactMethod
483+
public void getInitialEvents(Promise promise) {
484+
promise.resolve(delayedEvents);
485+
}
486+
413487
@ReactMethod
414488
public void setOnHold(String uuid, boolean shouldHold) {
415489
Log.d(TAG, "[VoiceConnection] setOnHold, uuid: " + uuid + ", shouldHold: " + (shouldHold ? "true" : "false"));
@@ -721,13 +795,21 @@ public void backToForeground() {
721795
}
722796
}
723797

724-
private void initializeTelecomManager() {
725-
Context context = this.getAppContext();
726-
ComponentName cName = new ComponentName(context, VoiceConnectionService.class);
727-
String appName = this.getApplicationName(context);
798+
public static void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) {
799+
int permissionsIndex = 0;
800+
List<String> permsList = Arrays.asList(permissions);
801+
for (int result : grantResults) {
802+
if (permsList.contains(grantedPermissions[permissionsIndex]) && result != PackageManager.PERMISSION_GRANTED) {
803+
hasPhoneAccountPromise.resolve(false);
804+
return;
805+
}
806+
permissionsIndex++;
807+
}
808+
hasPhoneAccountPromise.resolve(true);
809+
}
728810

729-
handle = new PhoneAccountHandle(cName, appName);
730-
telecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
811+
private boolean isSelfManaged() {
812+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && _settings.hasKey("selfManaged") && _settings.getBoolean("selfManaged");
731813
}
732814

733815
private void registerPhoneAccount(Context appContext) {
@@ -761,8 +843,18 @@ private void registerPhoneAccount(Context appContext) {
761843
}
762844

763845
private void sendEventToJS(String eventName, @Nullable WritableMap params) {
764-
Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName :" + eventName + ", args : " + (params != null ? params.toString() : "null"));
765-
this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(eventName, params);
846+
boolean isBoundToJS = this.reactContext.hasActiveCatalystInstance();
847+
Log.v(TAG, "[VoiceConnection] sendEventToJS, eventName: " + eventName + ", bound: " + isBoundToJS + ", hasListeners: " + hasListeners + " args : " + (params != null ? params.toString() : "null"));
848+
849+
if (isBoundToJS && hasListeners) {
850+
this.reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(eventName, params);
851+
} else {
852+
if (params == null) {
853+
params = Arguments.createMap();
854+
}
855+
params.putString("name", eventName);
856+
delayedEvents.pushMap(params);
857+
}
766858
}
767859

768860
private String getApplicationName(Context appContext) {
@@ -797,6 +889,7 @@ private static boolean hasPhoneAccount() {
797889

798890
private void registerReceiver() {
799891
if (!isReceiverRegistered) {
892+
voiceBroadcastReceiver = new VoiceBroadcastReceiver();
800893
IntentFilter intentFilter = new IntentFilter();
801894
intentFilter.addAction(ACTION_END_CALL);
802895
intentFilter.addAction(ACTION_ANSWER_CALL);
@@ -820,19 +913,6 @@ private Context getAppContext() {
820913
return this.reactContext.getApplicationContext();
821914
}
822915

823-
public static void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) {
824-
int permissionsIndex = 0;
825-
List<String> permsList = Arrays.asList(permissions);
826-
for (int result : grantResults) {
827-
if (permsList.contains(grantedPermissions[permissionsIndex]) && result != PackageManager.PERMISSION_GRANTED) {
828-
hasPhoneAccountPromise.resolve(false);
829-
return;
830-
}
831-
permissionsIndex++;
832-
}
833-
hasPhoneAccountPromise.resolve(true);
834-
}
835-
836916
private class VoiceBroadcastReceiver extends BroadcastReceiver {
837917
@Override
838918
public void onReceive(Context context, Intent intent) {

android/src/main/java/io/wazo/callkeep/RNCallKeepPackage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class RNCallKeepPackage implements ReactPackage {
3030

3131
@Override
3232
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
33-
return Collections.<NativeModule>singletonList(new RNCallKeepModule(reactContext));
33+
return Collections.<NativeModule>singletonList(RNCallKeepModule.getInstance(reactContext, true));
3434
}
3535

3636
// Deprecated RN 0.47

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ declare module 'react-native-callkeep' {
8383

8484
static registerAndroidEvents(): void
8585

86+
static unregisterAndroidEvents(): void
87+
8688
static displayIncomingCall(
8789
uuid: string,
8890
handle: string,

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ class RNCallKeep {
6262
RNCallKeepModule.registerEvents();
6363
};
6464

65+
unregisterAndroidEvents = () => {
66+
if (isIOS) {
67+
return;
68+
}
69+
RNCallKeepModule.unregisterEvents();
70+
};
71+
6572
hasDefaultPhoneAccount = async (options) => {
6673
if (!isIOS) {
6774
return this._hasDefaultPhoneAccount(options);

0 commit comments

Comments
 (0)