diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e887967e0..1dc8eb802 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -13,6 +13,7 @@
+
-
+
+
+
+
diff --git a/app/products/calls/connection/connection.ts b/app/products/calls/connection/connection.ts
index 6d736e3ff..d0efda6f7 100644
--- a/app/products/calls/connection/connection.ts
+++ b/app/products/calls/connection/connection.ts
@@ -8,6 +8,11 @@ import InCallManager from 'react-native-incall-manager';
import {mediaDevices, MediaStream, MediaStreamTrack, RTCPeerConnection} from 'react-native-webrtc';
import {setPreferredAudioRoute, setSpeakerphoneOn} from '@calls/actions/calls';
+import {
+ foregroundServiceStart,
+ foregroundServiceStop,
+ foregroundServiceSetup,
+} from '@calls/connection/foreground_service';
import {processMeanOpinionScore, setAudioDeviceInfo} from '@calls/state';
import {AudioDevice, type AudioDeviceInfo, type AudioDeviceInfoRaw, type CallsConnection} from '@calls/types/calls';
import {getICEServersConfigs} from '@calls/utils';
@@ -26,6 +31,11 @@ const rtcMonitorInterval = 4000;
const InCallManagerEmitter = new NativeEventEmitter(NativeModules.InCallManager);
+// Setup the foreground service channel
+if (Platform.OS === 'android') {
+ foregroundServiceSetup();
+}
+
export async function newConnection(
serverUrl: string,
channelID: string,
@@ -111,6 +121,10 @@ export async function newConnection(
audioDeviceChanged?.remove();
wiredHeadsetEvent?.remove();
+ if (Platform.OS === 'android') {
+ foregroundServiceStop();
+ }
+
if (closeCb) {
closeCb();
}
@@ -249,6 +263,9 @@ export async function newConnection(
}
}
});
+
+ // To allow us to use microphone in the background
+ await foregroundServiceStart();
}
// We default to speakerphone, but not if the WiredHeadset is plugged in.
diff --git a/app/products/calls/connection/foreground_service.ts b/app/products/calls/connection/foreground_service.ts
new file mode 100644
index 000000000..4df447624
--- /dev/null
+++ b/app/products/calls/connection/foreground_service.ts
@@ -0,0 +1,40 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import VIForegroundService from '@voximplant/react-native-foreground-service';
+
+import {logError} from '@utils/log';
+
+const channelConfig = {
+ id: 'calls_channel',
+ name: 'Mattermost',
+ description: 'Mattermost Calls microphone while app is in the background',
+ enableVibration: false,
+};
+
+// Note: multiple calls with same arguments are a noop.
+export const foregroundServiceSetup = () => {
+ VIForegroundService.getInstance().createNotificationChannel(channelConfig);
+};
+
+export const foregroundServiceStart = async () => {
+ const notificationConfig = {
+ channelId: 'calls_channel',
+ id: 345678,
+ title: 'Mattermost',
+ text: 'Mattermost Calls Microphone',
+ icon: '',
+ button: 'Stop',
+ };
+ try {
+ await VIForegroundService.getInstance().startService(notificationConfig);
+ } catch (e) {
+ logError('Calls: Cannot start ForegroundService, error:', e);
+ }
+};
+
+export const foregroundServiceStop = async () => {
+ await VIForegroundService.getInstance().stopService();
+};
diff --git a/jest.config.js b/jest.config.js
index 8ef10d316..ac7210c25 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -19,7 +19,7 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/test/file_transformer.js',
},
transformIgnorePatterns: [
- 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch|@mattermost/calls)',
+ 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch|@mattermost/calls|@voximplant/react-native-foreground-service)',
],
moduleNameMapper: {
diff --git a/package-lock.json b/package-lock.json
index e5f9efc16..0f08eb554 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
"@sentry/react-native": "5.9.0",
"@stream-io/flat-list-mvcp": "0.10.3",
"@tsconfig/react-native": "3.0.2",
+ "@voximplant/react-native-foreground-service": "3.0.2",
"base-64": "1.0.0",
"commonmark": "npm:@mattermost/commonmark@0.30.1-1",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#235bc817bcade503fb81fa51bbbe3c84f958ed12",
@@ -7645,6 +7646,14 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@voximplant/react-native-foreground-service": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz",
+ "integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==",
+ "peerDependencies": {
+ "react-native": ">= 0.41.2"
+ }
+ },
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -28508,6 +28517,12 @@
}
}
},
+ "@voximplant/react-native-foreground-service": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz",
+ "integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==",
+ "requires": {}
+ },
"@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
diff --git a/package.json b/package.json
index 322d4352f..00059f2e3 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@sentry/react-native": "5.9.0",
"@stream-io/flat-list-mvcp": "0.10.3",
"@tsconfig/react-native": "3.0.2",
+ "@voximplant/react-native-foreground-service": "3.0.2",
"base-64": "1.0.0",
"commonmark": "npm:@mattermost/commonmark@0.30.1-1",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#235bc817bcade503fb81fa51bbbe3c84f958ed12",