/*
* Copyright (c) Huawei Device Co., Ltd. 2024-2025. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import audio from '@ohos.multimedia.audio';
import {
DeviceHelper,
obtainLocalEvent,
obtainStartAbilityWithWant,
sEventManager
} from '@ohos/frameworkwrapper';
import { CheckEmptyUtils, LogDomain, LogHelper } from '@ohos/basicutils';
import { ExtAppConstants } from '@ohos/commonconstants';
import { BusinessError } from '@kit.BasicServicesKit';
import { Want } from '@kit.AbilityKit';
import { inputConsumer } from '@kit.InputKit';
import { volumeDownKeyOptions, volumeUpKeyOptions } from '../common/KeyOptions';
import { BaseController, CommonData } from '@ohos/controlcentercommon/Index';
import { AudioHelper } from '../utils/AudioHelper';
import { HashMap } from '@kit.ArkTS';
import { EventManagerAdapter } from '@ohos/systemuicommon/src/main/ets/adapter/EventManagerAdapter';
export interface VolumeComponentStyle {
width: number;
height: number;
bgColor: ResourceStr;
selectedBgColor: ResourceStr;
bgBlur: number;
}
export class VolumeConstants {
public static readonly DEFAULT_MAX_VOLUME: number = 15;
public static readonly DEFAULT_MIN_VOLUME: number = 0;
public static readonly DEFAULT_MIN_VOLUME_ONE: number = 1;
public static readonly DEFAULT_VOLUME_INVALID: number = -1;
}
export const VOLUME_COMPONENT_ID = 'volumeComponent'
export function getVolumeStyle(): VolumeComponentStyle {
return {
width: 96,
height: 324,
bgColor: $r('app.color.volume_bg_color'),
selectedBgColor: $r('app.color.volume_selected_bg_color'),
bgBlur: 45
}
}
/**
* 音量监听回调函数
*/
type VolumeChangeCallback = (value: number, type: audio.AudioVolumeType, needChangeTye: boolean) => void;
/**
* 音量相关参数
*/
export class VolumeValueInfo {
public volumePercentage: number = VolumeConstants.DEFAULT_VOLUME_INVALID; // 音量ui值默认百分比值
public currentVolumeValue: number = VolumeConstants.DEFAULT_MAX_VOLUME; // 当前音量值
public maxVolumeValue: number = VolumeConstants.DEFAULT_MAX_VOLUME; // 最大音量
public minVolumeValue: number = VolumeConstants.DEFAULT_MIN_VOLUME; // 最小音量
}
const TAG = 'volumeVM';
const log: LogHelper = LogHelper.getLogHelper(LogDomain.CC, TAG);
const MAX_RETRY_TIMES: number = 5; // AudioManager最多重试次数.
const RETRY_DELAY_TIMES: number = 1500; //重试间隔时间
const REFRESH_ICON_TIMEOUT: number = 280;
export class VolumeController extends BaseController<CommonData> {
public lottieData: object | null = null;
private static instance?: VolumeController;
// 响铃模式
public ringMode: audio.AudioRingMode = audio.AudioRingMode.RINGER_MODE_NORMAL;
private audioManager: audio.AudioManager | null = null;
private audioVolumeManager: audio.AudioVolumeManager | null = null;
private audioStreamManager: audio.AudioStreamManager | null = null;
private audioVolumeGroupManager: audio.AudioVolumeGroupManager | null = null;
// 记录不同音频流类型当前的音量信息
private volumeInfoMap: Map<audio.AudioVolumeType, VolumeValueInfo> = new Map([
[audio.AudioVolumeType.VOICE_CALL, new VolumeValueInfo()],
[audio.AudioVolumeType.RINGTONE, new VolumeValueInfo()],
[audio.AudioVolumeType.MEDIA, new VolumeValueInfo()],
[audio.AudioVolumeType.ALARM, new VolumeValueInfo()]
]);
// 音量调整最小只能调整到1的音频类型集合
private minVolumeOneSet: Set<audio.AudioVolumeType> =
new Set([audio.AudioVolumeType.ALARM, audio.AudioVolumeType.VOICE_CALL]);
// 当前正在生效的音频流类型
private currentVolumeType: audio.AudioVolumeType = audio.AudioVolumeType.MEDIA;
private callbackMap: HashMap<string, VolumeChangeCallback> = new HashMap();
private refreshIconTimeout: number | null = null;
constructor() {
super();
this.loadAudioManager(0);
}
static getInstance(): VolumeController {
if (!VolumeController.instance) {
VolumeController.instance = new VolumeController();
}
return VolumeController.instance;
}
/**
* 加载AudioManager
* @param reloadTimes 当前重试次数
*/
private loadAudioManager(reloadTimes: number) {
if (reloadTimes >= MAX_RETRY_TIMES) {
log.showInfo(`fail init AudioManager`);
return;
}
try {
this.audioManager = audio.getAudioManager();
this.audioVolumeManager = this.audioManager.getVolumeManager();
this.audioVolumeManager.on('volumeChange', this.onVolumeChange);
this.audioVolumeGroupManager = this.audioVolumeManager.getVolumeGroupManagerSync(audio.DEFAULT_VOLUME_GROUP_ID);
this.initAllVolumeInfo();
this.onRingModeEventReceived(this.audioVolumeGroupManager.getRingerModeSync());
this.audioVolumeGroupManager?.on('ringerModeChange', (ringerMode) => {
this.onRingModeEventReceived(ringerMode);
});
this.audioVolumeManager.on('activeVolumeTypeChange', this.updateVolumeUICallback);
log.showInfo(`init volumeVM success audioVolumeManager:${this.audioVolumeManager}
audioVolumeGroupManager:${this.audioVolumeGroupManager}`);
this.audioStreamManager = this.audioManager.getStreamManager();
this.audioStreamManager.on('audioCapturerChange',
(audioCapturerChangeInfoArray: Array<audio.AudioCapturerChangeInfo>) => {
this.updateCapturerUICallback(audioCapturerChangeInfoArray);
});
} catch (err) {
log.showError(`fail init volumeVM ,err: ${err}, reloadTime ${reloadTimes}`);
this.releaseAudioManagerCallback();
setTimeout(() => {
this.loadAudioManager(reloadTimes + 1);
}, RETRY_DELAY_TIMES)
}
}
public getVolumePercentage(): number {
return this.volumeInfoMap.get(this.currentVolumeType)?.volumePercentage as number;
}
public setVolumePercentage(volumePercentage: number): void {
let volumeInfo = this.volumeInfoMap.get(this.currentVolumeType);
if (volumeInfo !== undefined) {
volumeInfo.volumePercentage = volumePercentage;
}
}
getData(): CommonData {
return new CommonData();
}
isAvailable(): boolean {
return true;
}
/**
* 监听响铃模式切换事件
* @param ringModeEvent
*/
private onRingModeEventReceived(ringModeEvent: audio.AudioRingMode): void {
log.showInfo(`listen ring mode is ${ringModeEvent}`);
let volumeInfo = this.volumeInfoMap.get(audio.AudioVolumeType.RINGTONE);
if (volumeInfo === undefined) {
return;
}
this.ringMode = ringModeEvent;
if (ringModeEvent !== audio.AudioRingMode.RINGER_MODE_NORMAL) {
volumeInfo.currentVolumeValue = 0;
} else {
volumeInfo.currentVolumeValue =
this.audioVolumeGroupManager?.getVolumeSync(audio.AudioVolumeType.RINGTONE) as number ??
VolumeConstants.DEFAULT_MAX_VOLUME;
}
if (this.currentVolumeType === audio.AudioVolumeType.RINGTONE) {
this.callbackMap.forEach((callback) => {
if (volumeInfo !== undefined) {
callback?.(volumeInfo.currentVolumeValue, this.currentVolumeType, true);
}
})
log.showInfo(`update current ring volume: ${volumeInfo.currentVolumeValue}`);
}
}
/**
* 初始化不同音频流类型对应的音量信息
*/
public initAllVolumeInfo(): void {
this.volumeInfoMap.forEach((volumeInfo, volumeType) => {
try {
// 最大值
let maxVolumeValue =
this.audioVolumeGroupManager?.getMaxVolumeSync(audio.AudioVolumeType.MEDIA) ??
VolumeConstants.DEFAULT_MAX_VOLUME;
volumeInfo.maxVolumeValue = maxVolumeValue;
// 最小值
let minVolumeValue =
this.audioVolumeGroupManager?.getMinVolumeSync(audio.AudioVolumeType.MEDIA) ??
VolumeConstants.DEFAULT_MIN_VOLUME;
volumeInfo.minVolumeValue = minVolumeValue;
// 当前值
let volumeValue = this.audioVolumeGroupManager?.getVolumeSync(volumeType) as number ??
VolumeConstants.DEFAULT_MAX_VOLUME;
volumeInfo.currentVolumeValue = volumeValue;
log.showInfo(`${volumeType} init volume value, max ${maxVolumeValue}, min ${minVolumeValue}, cur ${volumeValue}`);
} catch (err) {
log.showError(`Failed to obtain volume value, error ${err}.`);
}
});
}
/**
* 控制中心显示时更新音量ui,切换至对应的音频通道
*/
public updateVolumeUI(): void {
try {
let audioVolumeType = this.audioVolumeGroupManager?.getActiveVolumeTypeSync(0);
log.showInfo(`get audioVolumeType ${audioVolumeType}`);
if (audioVolumeType !== undefined && this.volumeInfoMap.has(audioVolumeType)) {
this.currentVolumeType = audioVolumeType;
}
} catch (err) {
log.showError(`Failed to get active volume type, error ${err}.`);
}
this.refreshVolumeIconDelay();
log.showInfo(`set currentVolumeType ${this.currentVolumeType}`);
}
private updateVolumeUICallback = (audioVolumeType: audio.AudioVolumeType) => {
log.showInfo(`updateVolumeUICallback: ${audioVolumeType}`);
if (audioVolumeType === undefined || !this.volumeInfoMap.has(audioVolumeType)) {
return;
}
this.currentVolumeType = audioVolumeType;
log.showInfo(`updateVolumeUICallback set currentVolumeType ${this.currentVolumeType}`);
this.refreshVolumeIconDelay();
}
private updateCapturerUICallback(audioCapturerChangeInfoArray: Array<audio.AudioCapturerChangeInfo>) {
if (CheckEmptyUtils.isEmptyArr(audioCapturerChangeInfoArray)) {
return;
}
log.showInfo(`updateCapturerUICallback size: ${audioCapturerChangeInfoArray.length}`);
audioCapturerChangeInfoArray.forEach((capturerInfo) => {
if (capturerInfo?.capturerInfo?.source === audio.SourceType.SOURCE_TYPE_VOICE_RECOGNITION) {
if (capturerInfo?.capturerState > audio.AudioState.STATE_RUNNING) {
this.updateVolumeUI();
}
}
});
}
private refreshVolumeIconDelay(): void {
if (this.refreshIconTimeout !== null) {
clearTimeout(this.refreshIconTimeout);
this.refreshIconTimeout = null;
}
this.refreshIconTimeout = setTimeout(() => {
log.showInfo(`refreshVolumeIconDelay curType=${this.currentVolumeType}`);
this.callbackMap.forEach((callback) => {
callback?.(this.volumeInfoMap.get(this.currentVolumeType)?.currentVolumeValue as number,
this.currentVolumeType, true);
});
this.refreshIconTimeout = null;
}, REFRESH_ICON_TIMEOUT);
}
/**
* 释放音量变化监听以及输出设备类型变化监听
*/
public releaseVolumeChangeCallback(): void {
this.releaseAudioManagerCallback();
AudioHelper.getInstance().offOutputDeviceChange();
}
/**
* 释放音量通道和录音监听
*/
private releaseAudioManagerCallback(): void {
try {
this.audioVolumeManager?.off('volumeChange', this.onVolumeChange);
this.audioVolumeManager?.off('activeVolumeTypeChange', this.updateVolumeUICallback);
this.audioStreamManager?.off('audioCapturerChange');
} catch (err) {
log.showError(`releaseAudioManager fail:${(err as BusinessError).code}, ${(err as BusinessError).message}`);
}
}
/**
* 是否需要加载lottie
*/
isLoadLottie(): boolean {
return true;
}
initLottieData(): void {
if (DeviceHelper.isPC()) {
return;
}
log.showInfo('initLottieData start');
this.loadLottieData('lottie_volume_media_cc.json', (value) => {
this.lottieData = value;
log.showInfo(`initLottieData success`);
sEventManager.publish(obtainLocalEvent('volumeLottieReady', true));
});
}
/**
* 移除lottie缓存
*/
removeLottieData(): void {
log.showInfo('remove volume LottieData');
this.lottieData = null;
this.isAlreadyLoadLottie = false;
sEventManager.publish(obtainLocalEvent('volumeLottieReady', false));
}
private onVolumeChange = (volumeEvent: audio.VolumeEvent) => {
log.showInfo(`onVolumeChange stream: ${volumeEvent.volumeType}, level: ${volumeEvent.volume} `);
let volumeInfo = this.volumeInfoMap.get(volumeEvent.volumeType);
if (volumeInfo !== undefined) {
volumeInfo.currentVolumeValue = volumeEvent.volume;
}
if (volumeEvent.volumeType === this.currentVolumeType) {
this.callbackMap.forEach((callback) => {
callback?.(volumeEvent.volume, volumeEvent.volumeType, false);
});
}
}
public getCurrentVolumeType(): audio.AudioVolumeType {
return this.currentVolumeType;
}
getMaxVolume() {
let volumeInfo = this.volumeInfoMap.get(this.currentVolumeType);
if (volumeInfo !== undefined) {
return volumeInfo.maxVolumeValue;
}
return VolumeConstants.DEFAULT_MAX_VOLUME;
}
getMinVolume() {
let volumeInfo = this.volumeInfoMap.get(this.currentVolumeType);
if (volumeInfo !== undefined) {
return volumeInfo.minVolumeValue;
}
return VolumeConstants.DEFAULT_MIN_VOLUME;
}
public getVolume(): number {
return this.volumeInfoMap.get(this.currentVolumeType)?.currentVolumeValue as number;
}
public setVolumeChangeCallback(callbackTag: string, callback?: VolumeChangeCallback): void {
if (callback === undefined) {
this.callbackMap.remove(callbackTag);
log.showInfo('VolumeChangeCallback remove:' + callbackTag);
} else {
this.callbackMap.set(callbackTag, callback);
log.showInfo('VolumeChangeCallback set:' + callbackTag);
}
}
setVolume(volume: number) {
if (this.getVolume() === volume) {
return;
}
this.audioVolumeGroupManager?.setVolume(this.currentVolumeType, volume, (error: BusinessError) => {
if (error) {
log.showError(`sound setVolume media ${volume} fail, ${(error as BusinessError).code} ${(error as BusinessError).message}`);
}
log.showInfo(`sound setVolume media ${volume}`);
})
}
adjustVolumeChange(adjustType: audio.VolumeAdjustType) {
if (this.audioVolumeGroupManager === undefined || this.audioVolumeGroupManager === null) {
log.showWarn('adjustVolumeDownByStep undefined');
this.releaseVolumeChangeCallback();
this.loadAudioManager(4);
}
this.audioVolumeGroupManager?.adjustVolumeByStep(adjustType, (err: BusinessError) => {
if (err) {
log.showError(`failed to adjust the volume up by step. ${err.code} ${err.message} ${this.getVolume()}`);
return;
} else {
log.showInfo(`success to adjust the volume up by step. ${this.getVolume()}`);
}
});
}
adjustVolumeUpByStep() {
log.showInfo('adjustVolumeUpByStep start');
if (this.getVolume() === undefined || this.getVolume() as number >= this.getMaxVolume()) {
log.showError(`adjustVolumeUpByStep.getVolume Value is : ${this.getVolume()}`);
return;
}
this.adjustVolumeChange(audio.VolumeAdjustType.VOLUME_UP)
}
adjustVolumeDownByStep() {
log.showInfo('adjustVolumeDownByStep start');
let minValue = this.minVolumeOneSet.has(this.currentVolumeType) ? VolumeConstants.DEFAULT_MIN_VOLUME_ONE :
VolumeConstants.DEFAULT_MIN_VOLUME;
if (this.getVolume() === undefined || this.getVolume() as number <= minValue) {
log.showError(`adjustVolumeDownByStep.getVolume Value is : ${this.getVolume()}`);
return;
}
this.adjustVolumeChange(audio.VolumeAdjustType.VOLUME_DOWN)
}
/**
* 跳转音频管家
*/
goToAudioAccessoryManager(mac: string) {
if (!mac) {
return;
}
const want = {
bundleName: ExtAppConstants.PKG_AUDIO_ACCESSORY_MANAGER,
abilityName: ExtAppConstants.ABILITY_AUDIO_ACCESSORY_MANAGER,
parameters: {
mac: mac,
'ohos.aafwk.param.callerBundleName': 'com.ohos.sceneboard'
}
} as Want;
EventManagerAdapter.startAbility(obtainStartAbilityWithWant(want));
}
registerKeyInputEvent(volumeUpKeyCallback: () => void,
volumeDownKeyCallback: () => void) {
try {
inputConsumer.on('key', volumeUpKeyOptions, volumeUpKeyCallback);
} catch (error) {
log.showInfo(`Subscribe VolumeUpKeyOptions failed, error: ${(error as BusinessError).code} ${(error as BusinessError).message}`);
}
try {
inputConsumer.on('key', volumeDownKeyOptions, volumeDownKeyCallback);
} catch (error) {
log.showError(`Subscribe VolumeDownKeyOptions failed, error: ${(error as BusinessError).code} ${(error as BusinessError).message}`);
}
}
unregisterKeyInputEvent(volumeUpKeyCallback: () => void, volumeDownKeyCallback: () => void) {
try {
inputConsumer.off('key', volumeUpKeyOptions, volumeUpKeyCallback);
} catch (error) {
log.showInfo(`Subscribe VolumeUpKeyOptions failed, error:${(error as BusinessError).code} ${(error as BusinessError).message}`);
}
try {
inputConsumer.off('key', volumeDownKeyOptions, volumeDownKeyCallback);
} catch (error) {
log.showError(`Subscribe VolumeDownKeyOptions failed, error:${(error as BusinessError).code} ${(error as BusinessError).message}`);
}
}
public volumeUpKeyCallback = () => {
log.showInfo(`inputConsumer callback volumeUpKeyCallback ${this.getVolume()}`);
this.adjustVolumeUpByStep();
}
public volumeDownKeyCallback = () => {
log.showInfo(`inputConsumer callback volumeDownKeyCallback ${this.getVolume()}`);
this.adjustVolumeDownByStep();
}
}