Skip to content

Commit 97f9a6a

Browse files
authored
Merge pull request #724 from saif-o99/master
Multiple fixes on Android
2 parents ef3d399 + 214959b commit 97f9a6a

File tree

6 files changed

+211
-12
lines changed

6 files changed

+211
-12
lines changed

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Self Managed calling apps are an advanced topic, and there are many steps involv
219219
| [setForegroundServiceSettings()](#setForegroundServiceSettings) | `Promise<void>` | ❌ | ✅ |
220220
| [canMakeMultipleCalls()](#canMakeMultipleCalls) | `Promise<void>` | ❌ | ✅ |
221221
| [setCurrentCallActive()](#setCurrentCallActive) | `Promise<void>` | ❌ | ✅ |
222+
| [checkIsInManagedCall()](#setAvailable) | `Promise<Boolean>` | ❌ | ✅ |
222223
| [isCallActive()](#isCallActive) | `Promise<Boolean>` | ✅ | ❌ |
223224
| [getCalls()](#getCalls) | `Promise<Object[]>` | ✅ | ❌ |
224225
| [displayIncomingCall()](#displayIncomingCall) | `Promise<void>` | ✅ | ✅ |
@@ -316,6 +317,16 @@ RNCallKeep.setCurrentCallActive(uuid);
316317
- `uuid`: string
317318
- The `uuid` used for `startCall` or `displayIncomingCall`
318319

320+
### checkIsInManagedCall
321+
_This feature is available only on Android._
322+
323+
Returns true if there is an active native call
324+
325+
```js
326+
RNCallKeep.checkIsInManagedCall();
327+
```
328+
329+
319330
### isCallActive
320331
_This feature is available only on IOS._
321332

@@ -741,6 +752,7 @@ RNCallKeep.registerAndroidEvents();
741752
| [silenceIncomingCall](#silenceIncomingCall) |||
742753
| [checkReachability](#checkReachability) |||
743754
| [didChangeAudioRoute](#didChangeAudioRoute) |||
755+
| [onHasActiveCall](#onHasActiveCall) |||
744756

745757
### didReceiveStartCallAction
746758

@@ -993,6 +1005,19 @@ RNCallKeep.addEventListener('checkReachability', () => {
9931005

9941006
```
9951007

1008+
### onHasActiveCall
1009+
1010+
_Android only._
1011+
1012+
A listener that tells the JS side if a native call has been answered while there was an active self-managed call
1013+
1014+
```js
1015+
RNCallKeep.addEventListener('onHasActiveCall', () => {
1016+
// eg: End active app call if native call is answered
1017+
});
1018+
1019+
```
1020+
9961021
## Example
9971022

9981023
A full example is available in the [example](https://github.com/react-native-webrtc/react-native-callkeep/tree/master/example) folder.

actions.js

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const RNCallKeepShowIncomingCallUi = 'RNCallKeepShowIncomingCallUi';
1919
const RNCallKeepOnSilenceIncomingCall = 'RNCallKeepOnSilenceIncomingCall';
2020
const RNCallKeepOnIncomingConnectionFailed = 'RNCallKeepOnIncomingConnectionFailed';
2121
const RNCallKeepDidChangeAudioRoute = 'RNCallKeepDidChangeAudioRoute';
22+
const RNCallKeepHasActiveCall = 'RNCallKeepHasActiveCall';
2223
const isIOS = Platform.OS === 'ios';
2324

2425
const didReceiveStartCallAction = handler => {
@@ -60,6 +61,9 @@ const didDisplayIncomingCall = handler => eventEmitter.addListener(RNCallKeepDid
6061
const didPerformSetMutedCallAction = handler =>
6162
eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data));
6263

64+
const onHasActiveCall = handler =>
65+
eventEmitter.addListener(RNCallKeepHasActiveCall, handler);
66+
6367
const didToggleHoldCallAction = handler =>
6468
eventEmitter.addListener(RNCallKeepDidToggleHoldAction, handler);
6569

@@ -103,4 +107,5 @@ export const listeners = {
103107
silenceIncomingCall,
104108
createIncomingConnectionFailed,
105109
didChangeAudioRoute,
110+
onHasActiveCall
106111
};

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

+161-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
package io.wazo.callkeep;
19-
19+
import com.facebook.react.bridge.LifecycleEventListener;
2020
import android.Manifest;
2121
import android.app.Activity;
2222
import android.content.BroadcastReceiver;
@@ -48,6 +48,8 @@
4848
import android.telecom.PhoneAccountHandle;
4949
import android.telecom.TelecomManager;
5050
import android.telephony.TelephonyManager;
51+
import android.telephony.TelephonyCallback;
52+
import android.telephony.PhoneStateListener;
5153
import android.util.Log;
5254

5355
import com.facebook.react.bridge.Arguments;
@@ -101,7 +103,7 @@
101103
import 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

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -218,17 +218,23 @@ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManage
218218
@Override
219219
public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
220220
VoiceConnectionService.hasOutgoingCall = true;
221-
String uuid = UUID.randomUUID().toString();
222221

223-
Log.d(TAG, "[VoiceConnectionService] onCreateOutgoingConnection, uuid:" + uuid);
222+
Bundle extras = request.getExtras();
223+
String callUUID = extras.getString(EXTRA_CALL_UUID);
224+
225+
if(callUUID == null || callUUID == ""){
226+
callUUID = UUID.randomUUID().toString();
227+
}
228+
229+
Log.d(TAG, "[VoiceConnectionService] onCreateOutgoingConnection, uuid:" + callUUID);
224230

225231
if (!isInitialized && !isReachable) {
226-
this.notReachableCallUuid = uuid;
232+
this.notReachableCallUuid = callUUID;
227233
this.currentConnectionRequest = request;
228234
this.checkReachability();
229235
}
230236

231-
return this.makeOutgoingCall(request, uuid, false);
237+
return this.makeOutgoingCall(request, callUUID, false);
232238
}
233239

234240
private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) {

index.d.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ declare module 'react-native-callkeep' {
1616
checkReachability: 'RNCallKeepCheckReachability';
1717
didResetProvider: 'RNCallKeepProviderReset';
1818
didLoadWithEvents: 'RNCallKeepDidLoadWithEvents';
19+
onHasActiveCall : 'onHasActiveCall';
1920
}
2021

2122
export type InitialEvents = Array<{
@@ -54,6 +55,7 @@ declare module 'react-native-callkeep' {
5455
checkReachability: undefined;
5556
didResetProvider: undefined;
5657
didLoadWithEvents: InitialEvents;
58+
onHasActiveCall : undefined;
5759
}
5860

5961
type HandleType = 'generic' | 'number' | 'email';
@@ -74,7 +76,7 @@ declare module 'react-native-callkeep' {
7476
defaultToSpeaker = 0x8,
7577
overrideMutedMicrophoneInterruption = 0x80,
7678
}
77-
79+
7880
export enum AudioSessionMode {
7981
default = 'AVAudioSessionModeDefault',
8082
gameChat = 'AVAudioSessionModeGameChat',
@@ -273,5 +275,10 @@ declare module 'react-native-callkeep' {
273275
static setCurrentCallActive(callUUID: string): void
274276

275277
static backToForeground(): void
278+
279+
/**
280+
* @descriptions Android Only, Check if there is active native call
281+
*/
282+
static checkIsInManagedCall(): Promise<boolean>
276283
}
277284
}

0 commit comments

Comments
 (0)