Skip to content
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ are not listed here.
### Added

- **Android:** `foregroundService.foregroundServiceType` sets the types the foreground service starts with (Android 10 and later), as a mask of `ServiceInfo.FOREGROUND_SERVICE_TYPE_*` values. An app can ring with `phoneCall` alone while its manifest also declares `microphone`; CallKeep keeps the types for the whole call, so adding `microphone` after the answer is up to the app's native code (see the option in `docs/setup.md`). Without the option, the service still starts with every type the manifest declares.
- **Android:** `endCallsOnHostDestroy: false` keeps the calls, and the app's process, when the React activity is destroyed, for calls that must outlive it (owned by native code, for instance). By default CallKeep still ends every call and kills the process.
- **Android:** `endCallsWhenReactNativeStops` (default `true`): when React Native's instance stops while the app keeps running (a reload, an over-the-air update), CallKeep ends the calls, which no JavaScript handles any more. Set it to `false` when native code owns them.
- **Android:** `RNCallKeepModule.setupNative(context, options)` and `RNCallKeepModule.displayIncomingCall(context, uuid, number, callerName, hasVideo)` set CallKeep up and show an incoming call from native code, before React Native runs or without it (self-managed mode, Android 8 and later), for instance from a Firebase messaging service. `VoiceConnectionService.nativeFocusListener` and `hasConnectionServiceFocus()` let native code follow Telecom's call focus (Android 9 and later). See [Setup from native code (Android)](docs/setup.md#setup-from-native-code-android).
- **iOS:** Events are also posted to `NotificationCenter`, so native code can observe the call's actions before a React Native runtime exists. See [Listening from native code (iOS)](docs/events.md#listening-from-native-code-ios).

### Changed

- **Android:** Calls no longer end, and the app's process is no longer killed, when the React activity is destroyed: a configuration change the activity doesn't handle (font size, language), Back on Android 11 and earlier, or swiping the app away from Recents ended the call and closed the app. Calls go on, as in other calling apps, and end when the process dies (Telecom ends them) or when React Native's instance stops (`endCallsWhenReactNativeStops`). Keep the call state and CallKeep's listeners outside your screens; see [Calls and the app's lifecycle](docs/setup.md#calls-and-the-apps-lifecycle-android).
- **Docs:** The Expo guide explains the config plugin's setup and what to add to it: the `audio` background mode and microphone text on iOS; on Android, the foreground service permissions, without which Android 14 and later refuse CallKeep's foreground service, and the `phoneCall|microphone` service types; `RECORD_AUDIO` with Expo SDK 54 and earlier. ([#849](https://github.com/react-native-webrtc/react-native-callkeep/pull/849), @sultanmyrza)
- **Docs:** The API reference now lists `setSettings`, `setReachable`, `reportConnectingOutgoingCallWithUUID`, `reportConnectedOutgoingCallWithUUID` (iOS) and `unregisterAndroidEvents` (Android), which were never documented. CI checks that every method matches across `index.js`, `index.d.ts` and the API reference's table, and that the relative links in the docs resolve.
- **Docs:** The iOS and Android installation guides are rewritten for autolinking: they still described `react-native link` and manual Xcode and Gradle steps from before React Native 0.60. They now follow the example app's setup, tested on devices: background modes, Recents and Siri (with or without scenes) and VoIP push on iOS; the manifest, foreground service types, runtime permissions, managed and self-managed modes on Android.
Expand Down
53 changes: 29 additions & 24 deletions android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.HeadlessJsTaskService;
Expand Down Expand Up @@ -826,32 +827,36 @@ public void onHostPause() {

@Override
public void onHostDestroy() {
// Calls that must outlive the activity (owned by native code, for instance) stay, and so
// does the process.
WritableMap settings = getSettings(this.getAppContext());
if (settings != null && settings.hasKey("endCallsOnHostDestroy") && !settings.getBoolean("endCallsOnHostDestroy")) {
Log.d(TAG, "[RNCallKeepModule] onHostDestroy: calls kept, endCallsOnHostDestroy is false");
return;
}
// 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());
// The activity is gone, not the call: an unhandled configuration change (font size,
// language), Back on Android 11 and earlier, or swiping the app away from Recents destroy it
// while the app's JavaScript keeps running. Calls go on, as in other calling apps. They end
// when the process dies (Telecom ends them) or when React Native stops (invalidate).
Log.d(TAG, "[RNCallKeepModule] onHostDestroy: the activity is destroyed, calls go on");
}

@Override
public void invalidate() {
// React Native's instance is going away (a reload, an over-the-air update): the JavaScript
// that handles the calls goes with it, so they end, unless native code owns them.
WritableMap settings = getSettings(this.getAppContext());
boolean endCalls = settings == null || !settings.hasKey("endCallsWhenReactNativeStops")
|| settings.getBoolean("endCallsWhenReactNativeStops");
if (endCalls) {
// React Native may invalidate on a background thread; connections live on the main one.
UiThreadUtil.runOnUiThread(() -> {
ArrayList<VoiceConnection> connections = new ArrayList<>(VoiceConnectionService.currentConnections.values());
Log.d(TAG, "[RNCallKeepModule] invalidate: ending " + connections.size() + " call(s)");
for (Connection connection : connections) {
connection.onDisconnect();
}
});
} else {
Log.d(TAG, "[RNCallKeepModule] invalidate: calls kept, endCallsWhenReactNativeStops is false");
}
this.stopListenToNativeCallsState();
super.invalidate();
}

@ReactMethod
public void rejectCall(String uuid) {
Log.d(TAG, "[RNCallKeepModule] rejectCall, uuid: " + uuid);
Expand Down
14 changes: 12 additions & 2 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ RNCallKeep.setup(options).then(accepted => {});
- `notificationTitle`: string
- `notificationIcon`: string (optional), the name of a `mipmap` resource, such as `ic_launcher`
- `foregroundServiceType`: number (optional, Android 10 and later), the types the service starts with, as a mask of [`ServiceInfo.FOREGROUND_SERVICE_TYPE_*`](https://developer.android.com/reference/android/content/pm/ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL) values: `4` for `phoneCall`, `132` for `phoneCall` and `microphone`. Each type must be declared on the service in the manifest. Without it, the service starts with every type the manifest declares. CallKeep starts the service when the call is created and keeps these types for the whole call: with `4` alone, an answered call has no microphone access while the app is in the background. To add `microphone` once the call is answered, call `startForeground(Constants.FOREGROUND_SERVICE_NOTIFICATION_ID, notification, types)` on `VoiceConnectionService.currentConnectionService` from native code, with both types
- `endCallsOnHostDestroy`: boolean (optional, default `true`)
When the React activity is destroyed, CallKeep ends every call and kills the app's process. Set it to `false` when your calls must outlive the activity, for instance when native code owns them: ending them is then up to your app.
- `endCallsWhenReactNativeStops`: boolean (optional, default `true`)
When React Native's instance stops while the app keeps running (a reload, an over-the-air update), CallKeep ends the calls: the JavaScript that handled them is gone. Set it to `false` when native code owns your calls, as with [setup from native code](#setup-from-native-code-android). See [Calls and the app's lifecycle](#calls-and-the-apps-lifecycle-android).
- `selfManaged`: boolean (optional)
When set to true, call keep will configure itself to run as a self managed connection service. See the [self-managed mode guide](guides/android-self-managed.md) and [Google's documentation](https://developer.android.com/guide/topics/connectivity/telecom/selfManaged).
- `displayCallReachabilityTimeout`: number in ms (optional)
Expand Down Expand Up @@ -142,6 +142,16 @@ It stores the settings and registers the phone account as `setup()` does, but cr

Telecom gives self-managed calls the call focus (Android 9 and later). To start a call's audio before React Native runs, set `VoiceConnectionService.nativeFocusListener`: `onFocusGained` tells when the call may use audio, and on `onFocusLost` release its audio and microphone, then call `connectionServiceFocusReleased()` on `VoiceConnectionService.currentConnectionService`. `VoiceConnectionService.hasConnectionServiceFocus()` reads the current state; it turns false when the last call ends, for which Telecom calls no `onFocusLost`.

## Calls and the app's lifecycle (Android)

A call lasts as long as the app can handle it, not as long as its screen:

- **The activity is destroyed**, while the app keeps running: a configuration change the activity doesn't handle (font size, language), Back on Android 11 and earlier, or the user swiping the app away from Recents. The call goes on, as in other calling apps; Telecom keeps the app's process running during a call. The user gets back to it from the app or its notification.
- **The app's process dies** (a crash, or the system stops it): Telecom ends its calls.
- **React Native's instance stops** while the process runs (a reload, an over-the-air update): CallKeep ends the calls, unless [`endCallsWhenReactNativeStops`](#options) is `false`.

So keep your call state, and register CallKeep's event listeners, outside your screens, once when the app starts (in `index.js`, for instance): a new activity renders the calls that are still going on. The [example app](../example/) does so.

## Foreground service (Android 11 and later)

Since Android 11, an app needs a [foreground service](https://developer.android.com/about/versions/11/privacy/foreground-services) to use the microphone in the background. Set the `foregroundService` key in the [`setup()` options](#options), and declare the service's types in the manifest: see [Android installation](android-installation.md#manifest).
3 changes: 2 additions & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ Changes to the library's JavaScript (`../index.js`, `../actions.js`) reload with
| **Audio routes** | Lists the speaker, receiver, Bluetooth and headset, and moves the active call to one. | `getAudioRoutes`, `setAudioRoute` |
| **Events** | Every event the library sends, with its payload. | all of them |
| **Android mode** | Switches between self-managed and managed mode and runs `setup` again. | `setup({ android: { selfManaged } })` |
| During a call (Android): change the font size, press **Back**, or swipe the app away from Recents | The call goes on. Reopen the app: it shows the call, which you can hang up. | [Calls and the app's lifecycle](../docs/setup.md#calls-and-the-apps-lifecycle-android) |
| Call a contact back from **Recents** (iOS) | iOS opens the app and passes the call to the scene delegate; the app starts it. | `RNCallKeep.continueUserActivity` → `didReceiveStartCallAction` without `callUUID` → `startCall` |

## Where to look

| File | What it shows |
|---|---|
| [`src/callkeep.ts`](src/callkeep.ts) | The `setup()` options, Android runtime permissions, call UUIDs |
| [`src/useCallKeep.ts`](src/useCallKeep.ts) | The event listeners and what an app does on each one; events received before JavaScript was listening (`didLoadWithEvents`) |
| [`index.js`](index.js), [`src/useCallKeep.ts`](src/useCallKeep.ts) | The event listeners, registered once at startup, and what an app does on each one; the call state, kept outside the screen; events received before JavaScript was listening (`didLoadWithEvents`) |
| [`App.tsx`](App.tsx) | The screen |
| [`ios/CallKeepExample/SceneDelegate.swift`](ios/CallKeepExample/SceneDelegate.swift) | The scene life cycle that iOS 27 requires, and calls from Recents, Contacts or Siri |
| [`ios/CallKeepExample/AppDelegate.swift`](ios/CallKeepExample/AppDelegate.swift) | Where an app handles VoIP pushes |
Expand Down
2 changes: 2 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import { startCallKeep } from './src/useCallKeep';

startCallKeep();
AppRegistry.registerComponent(appName, () => App);
Loading
Loading