@react-native-oh-tpl/react-native-incall-manager
Version:
Handling media-routes/sensors/events during a audio/video chat on React Native
534 lines (496 loc) • 19.9 kB
text/typescript
/*
* MIT License
*
* Copyright (C) 2024 Huawei Device Co., Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { TurboModule, type TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
import window from '@ohos.window';
import type common from '@ohos.app.ability.common';
import { type BusinessError } from '@kit.BasicServicesKit';
import { sensor } from '@kit.SensorServiceKit';
import { avSession } from '@kit.AVSessionKit';
import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
import type { resourceManager } from '@kit.LocalizationKit';
import {
InCallManagerEventType,
type MediaType,
MediaTypeEnum,
ToneUriFromType,
PlayCategoryType,
} from './index';
import ContinueBackgroundTaskModel from './model/ContinueBackgroundTaskModel';
import ProximityLockUtil from './utils/ProximityLockUtil';
import AudioRoutingManagerUtil from './utils/AudioRoutingManagerUtil';
import VolumeManagerUtil from './utils/VolumeManagerUtil';
import FlashUtil from './utils/FlashUtil';
import AudioFileUtil from './utils/AudioFileUtil';
import PlayModel from './model/PlayModel';
import AudioSessionNotificationUtil from './utils/AudioSessionNotificationUtil';
import Logger from './Logger';
const TAG: string = 'InCallManagerTurboModule';
export class RNInCallManagerTurboModule extends TurboModule {
static AVSESSION_TAG: string = 'avSession';
static AVSESSION_TYPE_VOICE_CALL: avSession.AVSessionType = 'voice_call';
static AVSESSION_TYPE_VIDEO_CALL: avSession.AVSessionType = 'video_call';
static AVSESSION_TYPE_VOICE: avSession.AVSessionType = 'audio';
static AVSESSION_TYPE_VIDEO: avSession.AVSessionType = 'video';
private windowClass: window.Window | null = null;
private continueBackgroundTask: ContinueBackgroundTaskModel;
private audioSession: avSession.AVSession;
private media: string;
private audioSessionInitialized: boolean = false;
private ringtone: PlayModel;
private ringBack: PlayModel;
private busyTone: PlayModel;
private isProximityRegistered: boolean = false;
private proximityIsNear: boolean = false;
private isAudioSessionRouteChangeRegistered: boolean = false;
private forceSpeakerOn: number = 0;
private inCallAudioMode: avSession.AVSessionType;
private isOrigAudioSetupStored: boolean = false;
private origIsSpeakerPhoneOn: boolean = false;
private origIsMicrophoneMute: boolean = false;
constructor(ctx: TurboModuleContext) {
super(ctx);
this.getWindowClass();
this.audioSession = null;
this.audioSessionInitialized = false;
this.ringtone = null;
this.ringBack = null;
this.busyTone = null;
this.isProximityRegistered = false;
this.proximityIsNear = false;
this.inCallAudioMode = RNInCallManagerTurboModule.AVSESSION_TYPE_VOICE_CALL;
this.forceSpeakerOn = 0;
this.media = MediaTypeEnum.AUDIO;
this.continueBackgroundTask = new ContinueBackgroundTaskModel(this.ctx.uiAbilityContext as common.UIAbilityContext);
}
public addListener(type?: string): void {
}
public removeListeners(type?: number): void {
}
public start(
media: MediaType,
auto: boolean,
ringBack: string): void {
if (this.audioSessionInitialized) {
return;
}
if (this.ringtone && this.ringtone.isPlaying()) {
Logger.info(TAG, 'stop ringtone');
this.stopRingtone();
}
this.media = media;
const isVideo = media === MediaTypeEnum.VIDEO;
this.inCallAudioMode = isVideo ? RNInCallManagerTurboModule.AVSESSION_TYPE_VIDEO_CALL :
RNInCallManagerTurboModule.AVSESSION_TYPE_VOICE_CALL;
if (!this.audioSession) {
avSession.createAVSession(this.ctx.uiAbilityContext, RNInCallManagerTurboModule.AVSESSION_TAG,
this.inCallAudioMode, (err: BusinessError, data: avSession.AVSession) => {
if (err) {
Logger.error(`CreateAVSession BusinessError: code: ${err.code}`);
} else {
this.audioSession = data;
AudioSessionNotificationUtil.startAudioSessionKeyEventNotification(this.audioSession,
(keyText: string, code: number) => {
this.sendMediaButtonEvent(keyText, code);
});
data.activate();
}
});
}
this.storeOriginalAudioSetup();
this.forceSpeakerOn = 0;
this.startAudioSessionNotification();
if (ringBack && ringBack.length > 0) {
this.startRingback(ringBack);
}
this.audioSessionInitialized = true;
}
public stop(busyToneUriType: string): Promise<void> {
if (!this.audioSessionInitialized) {
return;
}
this.stopRingback();
if (busyToneUriType.length > 0 && this.startBusyTone(busyToneUriType)) {
return;
} else {
this.restoreOriginalAudioSetup();
this.stopBusyTone();
this.stopAudioSessionNotification();
this.setSpeakerphoneOn(false);
this.setMicrophoneMute(false);
if (this.audioSession) {
this.audioSession.deactivate((err: BusinessError) => {
if (err) {
Logger.error(`deactivate BusinessError: code: ${err.code}`);
}
this.audioSession.destroy();
this.audioSession = null;
});
}
this.forceSpeakerOn = 0;
this.audioSessionInitialized = false;
}
}
public turnScreenOff(): void {
if (!ProximityLockUtil.isSupportedProximityLock()) {
Logger.error(`The current device turnScreenOff call is not Supported.`);
return;
}
try {
ProximityLockUtil.lock();
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`The turnScreenOff call failed. error code: ${err.code}`);
}
}
public turnScreenOn(): void {
if (!ProximityLockUtil.isSupportedProximityLock()) {
Logger.error(`The current device turnScreenOn call is not Supported.`);
return;
}
try {
ProximityLockUtil.removeLock();
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`The turnScreenOn call failed. error code: ${err.code}`);
}
}
public getIsWiredHeadsetPluggedIn(): Promise<{ isWiredHeadsetPluggedIn: boolean }> {
return new Promise((resolve) => {
let isWiredHeadsetPluggedIn = AudioRoutingManagerUtil ? AudioRoutingManagerUtil.isWiredHeadsetPluggedIn() : false;
resolve({ isWiredHeadsetPluggedIn: isWiredHeadsetPluggedIn });
});
}
public setFlashOn(enable: boolean): void {
FlashUtil.setFlashOn(this.ctx.uiAbilityContext, enable);
}
public setKeepScreenOn(enable: boolean): void {
try {
this.windowClass.setWindowKeepScreenOn(enable);
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`The setKeepScreenOn call failed. error code: ${err.code}`);
}
}
public setSpeakerphoneOn(enable: boolean): void {
Logger.error(TAG, 'current not support setSpeakerphoneOn');
}
public setForceSpeakerphoneOn(flag: number): void {
if (flag < -1 || flag > 1) {
return;
}
this.forceSpeakerOn = flag;
Logger.error(TAG, 'current not support setForceSpeakerphoneOn');
}
public setMicrophoneMute(enable: boolean): void {
if (enable !== VolumeManagerUtil.isMicrophoneMuteSync()) {
Logger.error(TAG, 'current not support setMicrophoneMute');
}
}
public startRingtone(
ringtoneUriType: string,
category: string,
): void {
try {
if (this.ringtone) {
if (this.ringtone.isPlaying()) {
Logger.info(TAG, 'startRingtone is already playing.');
return;
} else {
this.stopRingtone();
}
}
if (VolumeManagerUtil.isRingerModeSilent()) {
Logger.info(TAG, "startRingtone(): ringer is silent. leave without play.");
return;
}
let ringtoneUri: resourceManager.RawFileDescriptor =
AudioFileUtil.getRingtoneUri(this.ctx.uiAbilityContext, ringtoneUriType);
if (!ringtoneUri) {
Logger.info(TAG, 'startRingtone: no available media');
return;
}
let audioRenderInfoObj: audio.AudioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_RINGTONE,
rendererFlags: 0
};
let fileFd: resourceManager.RawFileDescriptor = ringtoneUri;
this.ringtone =
new PlayModel(this.ctx.uiAbilityContext, false, (interruptCode: number, interruptText: string) => {
let data: { eventText: string, eventCode: number } = {
eventText: interruptText,
eventCode: interruptCode,
};
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.ON_AUDIO_FOCUS_CHANGE_TYPE, data);
});
this.ringtone.setOnPlayComplete((isComplete: boolean) => {
});
this.ringtone.prepareWithPlayFd(fileFd, audioRenderInfoObj, true);
if (category === PlayCategoryType.PLAY_BACK && this.continueBackgroundTask) {
this.continueBackgroundTask.startContinueBackgroundTask();
}
} catch (error) {
Logger.error(TAG, `Failed to startRingtone. ${error.code}`);
}
}
public stopRingtone(): void {
if (this.continueBackgroundTask) {
this.continueBackgroundTask.stopContinueBackgroundTask();
}
if (this.ringtone && this.ringtone.avPlayer) {
this.ringtone.release();
this.ringtone = null;
}
}
public startProximitySensor(): void {
if (this.isProximityRegistered === true) {
return;
}
try {
if (AudioRoutingManagerUtil.checkAudioRoute([audio.DeviceType.EARPIECE], audio.DeviceFlag.OUTPUT_DEVICES_FLAG)) {
this.turnScreenOff();
}
sensor.on(sensor.SensorId.PROXIMITY, (data: sensor.ProximityResponse) => {
let state: boolean = data.distance === 0;
if (state !== this.proximityIsNear) {
this.proximityIsNear = state;
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.PROXIMITY_TYPE, { isNear: state });
}
}, { interval: 'normal' });
this.isProximityRegistered = true;
} catch (error) {
let e: BusinessError = error as BusinessError;
Logger.error(`The startProximitySensor call failed. error code: ${e.code}`);
}
}
public stopProximitySensor(): void {
if (this.isProximityRegistered === false) {
return;
}
try {
if (AudioRoutingManagerUtil.checkAudioRoute([audio.DeviceType.EARPIECE], audio.DeviceFlag.OUTPUT_DEVICES_FLAG)) {
this.turnScreenOn();
}
sensor.off(sensor.SensorId.PROXIMITY);
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`The stopProximitySensor call failed. error code: ${err.code}`);
}
this.isProximityRegistered = false;
}
public startRingback(ringBackUriType: string): void {
try {
if (this.ringBack && this.ringBack.isPlaying()) {
Logger.info(TAG, 'startRingback is already playing.');
return;
} else if (this.ringBack && !this.ringBack.isPlaying()) {
this.stopRingback();
}
let ringBackUriTypeNew: string =
ringBackUriType === ToneUriFromType.DTMF ? ToneUriFromType.DEFAULT : ringBackUriType;
let ringBackUri: resourceManager.RawFileDescriptor =
AudioFileUtil.getRingBackUri(this.ctx.uiAbilityContext, ringBackUriTypeNew);
if (!ringBackUri) {
Logger.info(TAG, 'startRingback: no available media');
return;
}
let isEarDevice: boolean = AudioRoutingManagerUtil.checkAudioRoute([audio.DeviceType.EARPIECE],
audio.DeviceFlag.OUTPUT_DEVICES_FLAG) ||
this.inCallAudioMode !== RNInCallManagerTurboModule.AVSESSION_TYPE_VIDEO_CALL;
let audioRenderInfoObj: audio.AudioRendererInfo = {
content: isEarDevice ? audio.ContentType.CONTENT_TYPE_SPEECH : audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0
};
let fileFd: resourceManager.RawFileDescriptor = ringBackUri;
this.ringBack =
new PlayModel(this.ctx.uiAbilityContext, false, (interruptCode: number, interruptText: string) => {
let data = {
eventText: interruptText,
eventCode: interruptCode,
};
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.ON_AUDIO_FOCUS_CHANGE_TYPE, data);
});
this.ringBack.prepareWithPlayFd(fileFd, audioRenderInfoObj, true);
if (this.continueBackgroundTask) {
this.continueBackgroundTask.startContinueBackgroundTask();
}
this.setSpeakerphoneOn(false);
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(TAG, `The startRingback call failed. error code: ${err.code}`);
}
}
public stopRingback(): void {
if (this.continueBackgroundTask) {
this.continueBackgroundTask.stopContinueBackgroundTask();
}
if (this.ringBack) {
this.ringBack.release();
this.ringBack = null;
}
}
public pokeScreen(timeout: number): void {
Logger.error(TAG, `The current device pokeScreen call is not Supported.`);
}
public getAudioUriJS(audioType: string, fileType: string): Promise<string> {
return new Promise((resolve) => {
let result: resourceManager.RawFileDescriptor =
AudioFileUtil.getAudioUriJS(this.ctx.uiAbilityContext, audioType, fileType);
let filePath: string = AudioFileUtil.getAudioPath(audioType, fileType);
if (result) {
let resultFile: fileIo.File = fileIo.dup(result.fd);
resolve(resultFile.path ? resultFile.path + filePath : '');
} else {
resolve('');
}
});
}
public chooseAudioRoute(route: string): Promise<string> {
Logger.error(TAG, 'not support chooseAudioRoute');
return new Promise((reject) => {
reject('not support chooseAudioRoute');
});
}
public requestAudioFocus(): Promise<string> {
return new Promise((reject) => {
reject('not support requestAudioFocus');
});
}
public abandonAudioFocus(): Promise<string> {
return new Promise((reject) => {
reject('not support abandonAudioFocus');
});
}
private startBusyTone(busyToneUriStatus: string): boolean {
try {
if (this.busyTone && this.busyTone.isPlaying()) {
return false;
} else if (this.busyTone && !this.busyTone.isPlaying()) {
this.stopBusyTone();
}
let busyToneUriType: string = busyToneUriStatus === ToneUriFromType.DTMF ? ToneUriFromType.DEFAULT
: busyToneUriStatus;
let busyToneUri: resourceManager.RawFileDescriptor =
AudioFileUtil.getBusyToneUri(this.ctx.uiAbilityContext, busyToneUriType);
if (!busyToneUri) {
Logger.info(TAG, 'startBusyTone: no available media');
return;
}
let audioRenderInfoObj: audio.AudioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_SONIFICATION,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0
};
let fileFd: resourceManager.RawFileDescriptor = busyToneUri;
this.busyTone =
new PlayModel(this.ctx.uiAbilityContext, false, (interruptCode: number, interruptText: string) => {
let data = {
eventText: interruptText,
eventCode: interruptCode,
};
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.ON_AUDIO_FOCUS_CHANGE_TYPE, data);
});
this.busyTone.setOnPlayComplete((isComplete: boolean) => {
this.stop('');
});
this.busyTone.prepareWithPlayFd(fileFd, audioRenderInfoObj, false);
return true;
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`The startBusyTone call failed. error code: ${err.code}`);
return false;
}
}
private stopBusyTone(): void {
if (this.busyTone) {
this.busyTone.release();
this.busyTone = null;
}
}
private storeOriginalAudioSetup(): void {
if (!this.isOrigAudioSetupStored) {
this.origIsSpeakerPhoneOn = AudioRoutingManagerUtil.isSpeakerphoneOn();
this.origIsMicrophoneMute = VolumeManagerUtil.isMicrophoneMuteSync();
this.isOrigAudioSetupStored = true;
}
}
private restoreOriginalAudioSetup(): void {
if (this.isOrigAudioSetupStored) {
this.setSpeakerphoneOn(this.origIsSpeakerPhoneOn);
this.setMicrophoneMute(this.origIsMicrophoneMute);
this.isOrigAudioSetupStored = false;
}
}
private startAudioSessionNotification(): void {
this.startAudioSessionRouteChangeNotification();
this.startProximitySensor();
this.setKeepScreenOn(true);
}
private stopAudioSessionNotification(): void {
AudioSessionNotificationUtil.stopAudioSessionKeyEventNotification(this.audioSession);
this.stopAudioSessionRouteChangeNotification();
this.stopProximitySensor();
this.setKeepScreenOn(false);
this.turnScreenOn();
}
private sendMediaButtonEvent(keyText: string, code: number): void {
let params: { eventText: string, eventCode: number } = {
eventText: keyText,
eventCode: code
};
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.MEDIA_BUTTON_TYPE, params);
}
private startAudioSessionRouteChangeNotification(): void {
if (this.isAudioSessionRouteChangeRegistered) {
return;
}
if (!AudioRoutingManagerUtil) {
return;
}
AudioRoutingManagerUtil.onDeviceChangeWithWiredHeadSet((param: { isPlugged: boolean, hasMic: boolean, deviceName: string },
isConnect: boolean) => {
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.WIRED_HEADSET_TYPE, param);
if (!isConnect) {
this.ctx.rnInstance.emitDeviceEvent(InCallManagerEventType.NOISY_AUDIO_TYPE, null);
}
});
this.isAudioSessionRouteChangeRegistered = true;
}
private stopAudioSessionRouteChangeNotification(): void {
if (!this.isAudioSessionRouteChangeRegistered) {
return;
}
if (!AudioRoutingManagerUtil) {
return;
}
AudioRoutingManagerUtil.offDeviceChange();
this.isAudioSessionRouteChangeRegistered = false;
}
private async getWindowClass(): Promise<void> {
this.windowClass = await window.getLastWindow(this.ctx.uiAbilityContext);
return;
}
}