Android 上的「安全音量」

文章详细解析了Android系统中安全音量(SafeMediaVolume)机制的工作原理,包括如何避免音量过大导致的听力损害,以及如何在应用程序中正确地设置音量。

原文 http://www.tuicool.com/articles/Afmaamn  作者 Yrom's Blog

一个小问题

在做Android MediaPlayer相关的开发时,可能都会遇到一个需求:自定义音量控制条,如触摸屏幕某块区域则随之调整媒体播放音量并显示自定义的提示条。

设置音量的代码一般形如下:

AudioManager audioManager = (AudioManager)context.getSystemService(AUDIO_SERVICE);
audioManager.setStreamVolume(STREAM_MUSIC, newIndex, 0); // flags 为0因为我们要自己做提示

但当 Android 设备插上耳机,为了避免音量过高伤害用户听力,会触发其“安全音量”(Safe Media Volume)机制,如果在未经用户确认允许使用大音量时,且这时设置音量 newIndex 超过其推荐阈值,则这段代码执行完你会发现毫无反应,播放的声音依然不会很大。

如何处理?

其实很简单,关键在于往往被人忽略的最后一个参数 flags

只要在设置音量后,复查一次当前值是否相当,如果比较小,则交由系统来显示音量提示对话框。而此时因欲设定的值超过推荐值,一般会触发「音量过高警告」提示用户,用户确认后即可设置成功。

代码如下:

audioManager.setStreamVolume(STREAM_MUSIC, newIndex, 0);
if (Build.VERSION.SDK_INT >= 18 && audioManager.getStreamVolume(STREAM_MUSIC) < newIndex) {
    audioManager.setStreamVolume(STREAM_MUSIC, newIndex, FLAG_SHOW_UI);
}

效果如图:(用哔哩哔哩Android 客户端播放视频时滑动屏幕调整音量)

ps:这个对话框实现各个ROM厂商不一定一致。

pps:这个对话框原生Android M 只会在20小时内提示一次,如果你点过了确定。

下面为废话时间。

何谓“安全音量”

说欧州学者发现越来越多的小年轻失聪,其中大都因为喜欢用便携式设备长时间听音乐,想想当年几乎人手一个的随声听、MP3播放器。

心怀天下的组织(欧盟)针对这一类便携式音乐播放器提出了对限制输出声压级(SPL)的要求和指标(EN 60950等),并指出需要在合适的地方有音量过高有损听力的警告语。

手机属于便携式设备也可以播放音乐,自然也在此列。

想了解更多可以阅读另一个组织WHO的一篇相关调查报告: Situation analysis for safe listening devices’ standards

AudioService 中关于Safe Media Volume的实现

扯回 Android 系统上来,AudioService 会在系统启动后初始化对于媒体播放“安全音量”相关的设置(下面的源码都来自于Android M源码):

一些与之相关的常量定义:

AudioService.java
public class AudioService extends IAudioService.Stub {
    // mSafeMediaVolumeState 当前媒体音量是否被限制的状态标识.
    // 设备启动时为 SAFE_MEDIA_VOLUME_NOT_CONFIGURED. 
    // 根据国家不同或是 SAFE_MEDIA_VOLUME_ACTIVE 或是 SAFE_MEDIA_VOLUME_DISABLED.
    // 当用户确认过后会调用 disableSafeMediaVolume() 设为 SAFE_MEDIA_VOLUME_INACTIVE.
    private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0;
    private static final int SAFE_MEDIA_VOLUME_DISABLED = 1;
    private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2;
    private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3;
    private Integer mSafeMediaVolumeState;
    // Mobile country code,这里主要用来区分国家
    private int mMcc = 0;
    // 推荐的安全音量
    private int mSafeMediaVolumeIndex;
    // mSafeMediaVolumeDevices 强制开启SafeVolume的输出设备(耳机)
    private final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET |
                                                AudioSystem.DEVICE_OUT_WIRED_HEADPHONE;
    // mMusicActiveMs 在禁用SafeVolume下的使用耳机的累计时长.
    // 当累计达到 UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX 时会自动开启SafeVolume
    private int mMusicActiveMs;
    private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
    private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
    private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;  // 30s after boot completed
    ...
}

构造方法:

AudioService.java
    public AudioService(Context context) {
        ...
        // 初始化当前SafeVolume的状态
        mSafeMediaVolumeState = new Integer(Settings.Global.getInt(mContentResolver, Settings.Global.AUDIO_SAFE_VOLUME_STATE, SAFE_MEDIA_VOLUME_NOT_CONFIGURED));
        // 真正的安全阈值会在后面的onConfigureSafeVolume()方法中赋予
        mSafeMediaVolumeIndex = mContext.getResources().getInteger(com.android.internal.R.integer.config_safe_media_volume_index) * 10;
        ...
    }
    ...

onSystemReady()

系统进程初始化时会调用 systemReady() ,最终走到 onSystemReady()

AudioService.java
    public void onSystemReady() {
        mSystemReady = true;
        ...
        // 往mAudioHandler中发安全音量相关的延时消息,消息会在 30s 后 onConfigureSafeVolume()方法中处理
        sendMsg(mAudioHandler,
                MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED,
                SENDMSG_REPLACE,
                0,
                0,
                TAG,
                SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
        ...
    }
    ...
}

onConfigureSafeVolume()

针对各个国家的各自标准配置推荐值初始化:

AudioService.java
private void onConfigureSafeVolume(boolean force, String caller) {
    synchronized (mSafeMediaVolumeState) {
        int mcc = mContext.getResources().getConfiguration().mcc;
        if ((mMcc != mcc) || ((mMcc == 0) && force)) {
            // 从 values 中获取具体的安全音量阈值
            mSafeMediaVolumeIndex =  mContext.getResources().getInteger(com.android.internal.R.integer.config_safe_media_volume_index) * 10;
            boolean safeMediaVolumeEnabled =  SystemProperties.getBoolean("audio.safemedia.force", false) || mContext.getResources().getBoolean(com.android.internal.R.bool.config_safe_media_volume_enabled);

            boolean safeMediaVolumeBypass =                     SystemProperties.getBoolean("audio.safemedia.bypass", false);

            // persitedSate 只会是 "disabled" 或者 "active",这个变量值作用于下次启动且不能是 "inactive"
            int persistedState;
            if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) {
                persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
                // 有可能在这个方法被调用之前用户已经设为"inactive"了.
                if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
                    if (mMusicActiveMs == 0) {
                        mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
                        // 降低音量到安全阈值
                        enforceSafeMediaVolume(caller);
                    } else {
                        // 用户已经确认过
                        mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
                    }
                }
            } else {
                persistedState = SAFE_MEDIA_VOLUME_DISABLED;
                mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
            }
            mMcc = mcc;
            // 发送保存 persistedState 的消息
            sendMsg(mAudioHandler,
                    MSG_PERSIST_SAFE_VOLUME_STATE,
                    SENDMSG_QUEUE,
                    persistedState,
                    0,
                    null,
                    0);
        }
    }
}

enforceSafeMediaVolume()

强制将输出到耳机的媒体音量降低到推荐值:

AudioService.java
private void enforceSafeMediaVolume(String caller) {
    VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
    int devices = mSafeMediaVolumeDevices; // 0x4|0x8 = 0xC
    int i = 0;
    while (devices != 0) {
        // device 的定义从0x0开始,见 android.media.AudioSystem
        int device = 1 << i++;
        // 判断设备需不需要应用SafeVolume
        if ((device & devices) == 0) {
            continue;
        }
        // 获取当前设备的音量状态
        int index = streamState.getIndex(device);
        if (index > mSafeMediaVolumeIndex) {
            // 设置内存缓存的音量值为推荐值 mSafeMediaVolumeIndex
            streamState.setIndex(mSafeMediaVolumeIndex, device, caller);
            // 发送“设置当前音量值到设备”的消息
            sendMsg(mAudioHandler,
                    MSG_SET_DEVICE_VOLUME,
                    SENDMSG_QUEUE,
                    device,
                    0,
                    streamState,
                    0);
        }
        devices &= ~device;
    }
}

为免废话过长就不详解 AudioService 其它无关本文主题的方法了。

下面进入正题,来看一下“安全音量警告”的弹窗是怎么触发的。

setStreamVolume()

通过 AudioManager.setStreamVolume() 会调到 AudioService 中的同名方法:

AudioService.java
private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
        String caller, int uid) {
    ...
    ensureValidStreamType(streamType);
    // 对于 streamType = STREAM_MUSIC 来说 ,streamTypeAlias = streamType = STREAM_MUSIC
    int streamTypeAlias = mStreamVolumeAlias[streamType];
    VolumeStreamState streamState = mStreamStates[streamTypeAlias];

    final int device = getDeviceForStream(streamType);
    int oldIndex;

    ...

    synchronized (mSafeMediaVolumeState) {
        // reset any pending volume command
        mPendingVolumeCommand = null;

        oldIndex = streamState.getIndex(device);

        index = rescaleIndex(index * 10, streamType, streamTypeAlias);

        ...
        // 当前用户未确认,且媒体音量超过推荐值,则会返回false
        if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
            // 通知 IVolumeController 处理安全音量的警告
            mVolumeController.postDisplaySafeVolumeWarning(flags);
            // 会在用户确认后执行真正的更新音量操作
            mPendingVolumeCommand = new StreamVolumeCommand(
                                                streamType, index, flags, device);
        } else {
            // 
            onSetStreamVolume(streamType, index, flags, device, caller);
            index = mStreamStates[streamType].getIndex(device);
        }
    }
    // 会调用IVolumeController.volumeChanged() 进行音量变更的相关提示
    sendVolumeUpdate(streamType, oldIndex, index, flags);
}

IVolumeController

音量相关控制逻辑会在 IVolumeController 这个远程服务中处理,因为涉及到 UI交互所以实现的比较“绕”,避免篇幅太长,就不一一详解了(想看更多请翻阅 com.android.systemui.volume 包下源码 )。

只需要知道 mVolumeController 其实是 VolumeDialogController 类中一个的内部类 VC 实例即可,而 VC 则与 VolumeDialog 之间通过 Callback 纠缠在一起。

SafetyWarningDialog.show()

通过 mVolumeController.postDisplaySafeVolumeWarning(flags) 传递的 flags 最终会走到 VolumeDialog 类中用来处理 SafetyWarningDialog 的显示逻辑

VolumeDialog.java
public class VolumeDialog {
    private void showSafetyWarningH(int flags) {
        if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0
                || mShowing) {
            synchronized (mSafetyWarningLock) {
                if (mSafetyWarning != null) {
                    return;
                }
                mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) {
                    @Override
                    protected void cleanUp() {
                        synchronized (mSafetyWarningLock) {
                            mSafetyWarning = null;
                        }
                        recheckH(null);
                    }
                };
                mSafetyWarning.show();
            }
            recheckH(null);
        }
        rescheduleTimeoutH();
    }
}

AudioManager.disableSafeMediaVolume()

当用户在 SafetyWarningDialog 确认后,会通过 AudioManager 回调到 AudioService 关闭安全音量模式,并执行上次遗留的 mPendingVolumeCommand

AudioService.java
public void disableSafeMediaVolume(String callingPackage) {
    enforceVolumeController("disable the safe media volume");
    synchronized (mSafeMediaVolumeState) {
        setSafeMediaVolumeEnabled(false, callingPackage);
        if (mPendingVolumeCommand != null) {
            onSetStreamVolume(mPendingVolumeCommand.mStreamType,
                              mPendingVolumeCommand.mIndex,
                              mPendingVolumeCommand.mFlags,
                              mPendingVolumeCommand.mDevice,
                              callingPackage);
            mPendingVolumeCommand = null;
        }
    }
}

<think>嗯,用户想了解如何配置Android设备耳机的安全音量等级。这个问题涉及系统级别的音频参数调整,需要谨慎处理。 用户提供的引用资料非常专业,包含了config.xml中相关配置项的说明。从引用[3]和[4]可以看出,安全音量主要涉及两个关键配置项:config_safe_media_volume_index定义安全音量等级值,config_safe_media_volume_enabled控制是否启用安全音量功能。 用户可能正在开发定制ROM或需要为企业设备配置特定音量限制。考虑到安全音量涉及听力保护法规,不同地区可能有不同要求,这点在引用[3]提到是"country specific"。 需要提醒用户注意几个要点:首先这些配置需要修改系统资源文件,普通应用无法更改;其次修改后需要重启生效;最后还要考虑设备兼容性,因为不同厂商可能对音频框架有定制。 用户似乎已经查阅了部分Android源码文档,说明有一定技术基础。可以适当深入解释实现机制,比如AudioService如何读取这些配置项,以及安全音量提示的触发逻辑。 在回答结构上,应该分步骤说明配置方法,同时强调风险:修改系统文件可能影响OTA升级,错误配置可能导致音量异常。最后补充测试验证方法会更完整。</think>在Android系统中配置耳机安全音量等级需要修改系统配置文件,以下是具体步骤和注意事项: ### 配置方法 1. **修改安全音量阈值** 在`frameworks/base/core/res/res/values/config.xml`文件中调整安全音量索引值: ```xml <!-- 默认值通常为8-15(范围0-15) --> <integer name="config_safe_media_volume_index">10</integer> ``` *值越小,触发安全音量音量阈值越低*[^3][^4]。 2. **启用/禁用安全音量功能** 同文件内控制功能开关: ```xml <bool name="config_safe_media_volume_enabled">true</bool> ``` - `true`:启用安全音量提示 - `false`:完全禁用该功能[^1][^5]。 3. **调整最大持续时长**(可选) 在代码层修改超时阈值(单位:毫秒): ```java // 位于AudioService.java private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = 72000000; // 20小时 ``` *超过此时长系统会自动将音量降至安全等级*[^2]。 ### 生效流程 ```mermaid graph LR A[修改config.xml] --> B[编译系统镜像] B --> C[刷入设备] C --> D[重启设备] D --> E[新配置生效] ``` ### 注意事项 1. **权限要求** - 需拥有系统源码编译环境 - 修改后需重新编译系统镜像[^3][^4] 2. **兼容性风险** - 厂商定制ROM可能覆盖此配置 - 部分设备需同步修改`audio_policy.conf`[^5] 3. **法规遵循** 欧盟等地区强制要求启用安全音量,禁用可能导致设备认证失效[^1]。 > **实测建议**:修改后插入耳机播放媒体,当音量超过设定阈值时,系统应弹出警告提示框(若未禁用提示功能)[^1][^4]。 --- ### 相关问题 1. **如何绕过安全音量提示而不修改系统文件?** *答:可通过`adb shell settings put system volume_music_safe 0`临时禁用,但重启失效且需root权限[^5]。* 2. **安全音量与媒体音量最大值的关系是什么?** *答:安全音量是独立阈值,当媒体音量超过该值且耳机插入时触发限制,二者采用同一音量等级体系(0-15级)[^3]。* 3. **为什么修改配置后安全提示仍出现?** *答:检查是否同时满足:①编译时正确集成修改 ②未触发厂商覆盖逻辑 ③`config_safe_media_volume_enabled`设为false[^4][^5]。* [^1]: 配置开关与提示功能关联性 [^2]: 安全音量超时机制实现 [^3]: 系统默认参数定义位置 [^4]: 关键配置项说明 [^5]: 属性读取优先级逻辑
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值