Skip to content

Multiple fixes on Android #724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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