Skip to content

Commit

Permalink
Merge pull request #724 from saif-o99/master
Browse files Browse the repository at this point in the history
Multiple fixes on Android
  • Loading branch information
manuquentin authored Aug 17, 2023
2 parents ef3d399 + 214959b commit 97f9a6a
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 12 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ Self Managed calling apps are an advanced topic, and there are many steps involv
| [setForegroundServiceSettings()](#setForegroundServiceSettings) | `Promise<void>` | ❌ | ✅ |
| [canMakeMultipleCalls()](#canMakeMultipleCalls) | `Promise<void>` | ❌ | ✅ |
| [setCurrentCallActive()](#setCurrentCallActive) | `Promise<void>` | ❌ | ✅ |
| [checkIsInManagedCall()](#setAvailable) | `Promise<Boolean>` | ❌ | ✅ |
| [isCallActive()](#isCallActive) | `Promise<Boolean>` | ✅ | ❌ |
| [getCalls()](#getCalls) | `Promise<Object[]>` | ✅ | ❌ |
| [displayIncomingCall()](#displayIncomingCall) | `Promise<void>` | ✅ | ✅ |
Expand Down Expand Up @@ -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._

Expand Down Expand Up @@ -741,6 +752,7 @@ RNCallKeep.registerAndroidEvents();
| [silenceIncomingCall](#silenceIncomingCall) |||
| [checkReachability](#checkReachability) |||
| [didChangeAudioRoute](#didChangeAudioRoute) |||
| [onHasActiveCall](#onHasActiveCall) |||

### didReceiveStartCallAction

Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -103,4 +107,5 @@ export const listeners = {
silenceIncomingCall,
createIncomingConnectionFailed,
didChangeAudioRoute,
onHasActiveCall
};
168 changes: 161 additions & 7 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -429,7 +551,8 @@ public void endAllCalls() {
Connection connectionToEnd = connectionEntry.getValue();
connectionToEnd.onDisconnect();
}

this.stopListenToNativeCallsState();
this.hasActiveCall = false;
Log.d(TAG, "[RNCallKeepModule] endAllCalls executed");
}

Expand Down Expand Up @@ -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<Map.Entry<String, VoiceConnection>> connections =
new ArrayList<Map.Entry<String, VoiceConnection>>(VoiceConnectionService.currentConnections.entrySet());
for (Map.Entry<String, VoiceConnection> 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);
Expand All @@ -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();
}

Expand Down
14 changes: 10 additions & 4 deletions android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare module 'react-native-callkeep' {
checkReachability: 'RNCallKeepCheckReachability';
didResetProvider: 'RNCallKeepProviderReset';
didLoadWithEvents: 'RNCallKeepDidLoadWithEvents';
onHasActiveCall : 'onHasActiveCall';
}

export type InitialEvents = Array<{
Expand Down Expand Up @@ -54,6 +55,7 @@ declare module 'react-native-callkeep' {
checkReachability: undefined;
didResetProvider: undefined;
didLoadWithEvents: InitialEvents;
onHasActiveCall : undefined;
}

type HandleType = 'generic' | 'number' | 'email';
Expand All @@ -74,7 +76,7 @@ declare module 'react-native-callkeep' {
defaultToSpeaker = 0x8,
overrideMutedMicrophoneInterruption = 0x80,
}

export enum AudioSessionMode {
default = 'AVAudioSessionModeDefault',
gameChat = 'AVAudioSessionModeGameChat',
Expand Down Expand Up @@ -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<boolean>
}
}
Loading

0 comments on commit 97f9a6a

Please sign in to comment.