From 1fb01dfcdc2d6123d128323357d987aeb3722ba9 Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Fri, 13 Oct 2023 14:45:14 -0400 Subject: [PATCH] MM-54553 - Calls: Enable microphone input in background (Android) (#7585) * Add foreground service for Android; add voip UIBackgroundMode for iOS * remove iOS change for this PR * create foreground notification channel once at startup --- android/app/src/main/AndroidManifest.xml | 6 ++- app/products/calls/connection/connection.ts | 17 ++++++++ .../calls/connection/foreground_service.ts | 40 +++++++++++++++++++ jest.config.js | 2 +- package-lock.json | 15 +++++++ package.json | 1 + 6 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 app/products/calls/connection/foreground_service.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e887967e08c..1dc8eb80281 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 6d736e3ff5c..d0efda6f7f2 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 00000000000..4df44762438 --- /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 8ef10d316a0..ac7210c25d6 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 e5f9efc1669..0f08eb554b3 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 322d4352f4d..00059f2e3a0 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",