Наверное, многие, кто слушает музыку (и не только) с Android-устройства, сталкивались с таким предупреждением:

В этой статье мы рассмотрим, почему и когда возникает данное предупреждение, и как сделать так, чтобы оно больше не возникало.
Появляется оно только при прослушивании аудио через внешнее устройство (наушники/колонки). Для тех, кто не встречался с таким, небольшое пояснение: представьте, что вы слушаете музыку в наушниках, довольно громкую. Внезапно звук становится тише. Вы пытаетесь прибавить громкость, используя кнопки на корпусе, но не выходит. Достав устройство из кармана и сняв блокировку, вы и увидите такое предупреждение. Только после согласия с ним можно будет прибавить громкость обратно.
Да, предупреждение разумное, но появляется оно в непредсказуемый момент, иногда самый неподходящий: когда вы в общественном транспорте/за рулём/зимой на улице/когда у вас грязные руки и т.д. Доставать устройство, снимать блокировку, соглашаться с предупреждением, класть устройство обратно в этих случаях неудобно. А в случае, если подключены колонки, а не наушники, сообщение не совсем уместно.
Данное предупреждение — не собственная инициатива авторов платформы. Всё дело в том, что существует WHO-ETU стандарт “безопасного прослушивания” (safe listening). В европейских и некоторых других странах его выполнение обязательно. В стандарте описывается, как долго можно прослушивать аудио в зависимости от громкости с минимальным риском снижения слуха. Например, для взрослого человека безопасная недельная “доза” звука — 1.6 Pa2h, что эквивалентно 20 часам прослушивания на громкости 83 dB.

В зависимости от mcc (mobile country code), режим safe listening может быть включен или выключен. Определяется это значением ресурса
Если режим включен, то система считает время прослушивания на небезопасной громкости (выше 85 dB), и периодически сохраняет значение в переменную
Такая реализация довольно простая и не учитывает, например, в течение какого времени пользователь прослушал эти 20 часов: возможно, за пару дней, а, может, слушал по 6-7 минут в течение полугода (в соответствии со стандартом это не является угрозой для слуха).
Логика safe listening сосредоточена в классе классе AudioService.java, в нём можно увидеть соответствующие поля:
Поле
Также есть поле
Метод проверки превышения лимита выглядит так:
Чтобы выключить safe listening, нужно добиться того, чтобы переменной
Посмотрим, где изначально задаётся значение:
Видим, что помимо значения ресурса
Чтобы отключить предупреждение, нужно установить значение audio.safemedia.bypass=true в файле system/build.properties. Но для этого нужны root-права. Если их нет, то нужно разбираться дальше и искать другой способ.
Давайте посмотрим, что происходит при закрытии диалога с предупреждением по нажатию ОК, и попробуем это воспроизвести:
Вызывается метод
Он помечен аннотацией
Вызов заканчивается исключением
Разрешение STATUS_BAR_SERVICE имеет protectionLevel=«signature|privileged», получить его не получится.
Что ж, тогда попробуем так. Мы будем следить за переменной
Прочитать значение
То же самое, используя adb:
А чтобы записать значение, приложению потребуется разрешение
Оно имеет protectionLevel=«signature|privileged|development», а значит его можно выдать приложению используя adb:
Само значение записать можно так:
То же самое можно сделать с помощью adb:
Сбрасывать лучше в 1, как это сделано в AudioManager, а не в 0. Так как 0 соответствует состоянию ACTIVE.
Теперь нужно, чтобы AudioService прочитал новое значение, и обновил значение локальной переменной
Есть подходящий метод в AudioManager.java
Он инициирует вызов метода
Метод помечен аннотацией
java.lang.SecurityException: Permission Denial: get/set setting for user asks to run as user -2 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL
Да, аннотация
Остаётся один способ заставить AudioService прочитать новое значение — его перезапуск. Просто так перезапустить системный сервис нельзя. Нужно или перезагрузить устройство, или переключиться на другого пользователя, а затем вернуться обратно.
Теперь настало время проверить теорию.
Устанавливаем
Перезапускаем устройство (можно вместо этого переключиться на другого пользователя, а потом вернуться):
Подключаем наушники, включаем музыку погромче. В течение минуты появляется диалог.
Теперь повторяем те же действия, но присваиваем unsafe_volume_music_active_ms = 1. Включаем музыку, ждём минуту. Диалог не появляется.
Чтобы отключить предупреждение, можно сделать следующее:
При наличии root-прав
Установить значение audio.safemedia.bypass=true в файле system/build.properties
Без root-прав
Нужно следить за значением
Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.
Производители устройств могут вносить изменения в код платформы, и, судя по комментариям, некоторые из них смягчают дефолтное поведение. Например, предупреждение может появиться один раз, и больше не возникать до следующей перезагрузки.

В этой статье мы рассмотрим, почему и когда возникает данное предупреждение, и как сделать так, чтобы оно больше не возникало.
Появляется оно только при прослушивании аудио через внешнее устройство (наушники/колонки). Для тех, кто не встречался с таким, небольшое пояснение: представьте, что вы слушаете музыку в наушниках, довольно громкую. Внезапно звук становится тише. Вы пытаетесь прибавить громкость, используя кнопки на корпусе, но не выходит. Достав устройство из кармана и сняв блокировку, вы и увидите такое предупреждение. Только после согласия с ним можно будет прибавить громкость обратно.
Да, предупреждение разумное, но появляется оно в непредсказуемый момент, иногда самый неподходящий: когда вы в общественном транспорте/за рулём/зимой на улице/когда у вас грязные руки и т.д. Доставать устройство, снимать блокировку, соглашаться с предупреждением, класть устройство обратно в этих случаях неудобно. А в случае, если подключены колонки, а не наушники, сообщение не совсем уместно.
Почему оно возникает
Данное предупреждение — не собственная инициатива авторов платформы. Всё дело в том, что существует WHO-ETU стандарт “безопасного прослушивания” (safe listening). В европейских и некоторых других странах его выполнение обязательно. В стандарте описывается, как долго можно прослушивать аудио в зависимости от громкости с минимальным риском снижения слуха. Например, для взрослого человека безопасная недельная “доза” звука — 1.6 Pa2h, что эквивалентно 20 часам прослушивания на громкости 83 dB.

Реализация
В зависимости от mcc (mobile country code), режим safe listening может быть включен или выключен. Определяется это значением ресурса
R.bool.config_safe_media_volume_enabled
.Если режим включен, то система считает время прослушивания на небезопасной громкости (выше 85 dB), и периодически сохраняет значение в переменную
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS
. Когда значение достигает 20 часов, выводится предупреждение. После согласия с предупреждением значение сбрасывается, и подсчёт начинается заново.Такая реализация довольно простая и не учитывает, например, в течение какого времени пользователь прослушал эти 20 часов: возможно, за пару дней, а, может, слушал по 6-7 минут в течение полугода (в соответствии со стандартом это не является угрозой для слуха).
Логика safe listening сосредоточена в классе классе AudioService.java, в нём можно увидеть соответствующие поля:
// mMusicActiveMs is the cumulative time of music activity since safe volume was disabled.
// When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
// automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
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
Поле
mMusicActiveMs
содержит число миллисекунд, прослушанных пользователем на небезопасной громкости со времени последнего подтверждения диалога. Начальное значение загружается из переменной Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS
. В эту же переменную каждую минуту записывается новое значение mMusicActiveMs.Также есть поле
mSafeMediaVolumeState
, оно содержит текущее состояние системы safe listening: DISABLED
: отключенаACTIVE
: включена, и при этом лимит прослушивания достигнут, а значит нельзя разрешать пользователю увеличивать громкость, пока он не согласится с предупреждениемINACTIVE
: включена, лимит пока не достигнут
// mSafeMediaVolumeState indicates whether the media volume is limited over headphones.
// It is SAFE_MEDIA_VOLUME_NOT_CONFIGURED at boot time until a network service is connected
// or the configure time is elapsed. It is then set to SAFE_MEDIA_VOLUME_ACTIVE or
// SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it
// can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume()
// (when user opts out).
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; // confirmed
private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3; // unconfirmed
private Integer mSafeMediaVolumeState;
Метод проверки превышения лимита выглядит так:
private void onCheckMusicActive(String caller) {
synchronized (mSafeMediaVolumeState) {
if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
if ((device & mSafeMediaVolumeDevices) != 0) {
sendMsg(mAudioHandler,
MSG_CHECK_MUSIC_ACTIVE,
SENDMSG_REPLACE,
0,
0,
caller,
MUSIC_ACTIVE_POLL_PERIOD_MS);
int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device);
if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0) &&
(index > safeMediaVolumeIndex(device))) {
// Approximate cumulative active music time
mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
setSafeMediaVolumeEnabled(true, caller);
mMusicActiveMs = 0;
}
saveMusicActiveMs();
}
}
}
}
}
Как отключить предупреждение
Чтобы выключить safe listening, нужно добиться того, чтобы переменной
mSafeMediaVolumeState
на этапе конфигурации было присвоено значение DISABLED
.Посмотрим, где изначально задаётся значение:
private void onConfigureSafeVolume(boolean force, String caller) {
...
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);
int persistedState;
if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) {
persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
/* Ещё код, присваивающий mSafeMediaVolumeState значение либо ACTIVE, либо INACTIVE */
...
} else {
persistedState = SAFE_MEDIA_VOLUME_DISABLED;
mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
}
Видим, что помимо значения ресурса
R.bool.config_safe_media_volume_enabled
, есть два свойства, позволяющих включать/выключать систему safe listening: audio.safemedia.force и audio.safemedia.bypass.Чтобы отключить предупреждение, нужно установить значение audio.safemedia.bypass=true в файле system/build.properties. Но для этого нужны root-права. Если их нет, то нужно разбираться дальше и искать другой способ.
Как отключить предупреждение без root
Давайте посмотрим, что происходит при закрытии диалога с предупреждением по нажатию ОК, и попробуем это воспроизвести:
@Override
public void onClick(DialogInterface dialog, int which) {
mAudioManager.disableSafeMediaVolume();
}
Вызывается метод
disableSafeMediaVolume
у инстанса AudioManager
. /**
* Only useful for volume controllers.
* @hide
*/
public void disableSafeMediaVolume() { … }
Он помечен аннотацией
@hide
. Это означает, что метод не будет включён в public API несмотря на модификатор public. До Android 9 это легко можно было обойти используя рефлекшн. Теперь же такой метод по-прежнему можно вызывать, но уже с помощью трюка под названием double-reflection:val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String::class.java, arrayOf<Class<*>>()::class.java)
val disableSafeMediaVolumeMethod = getDeclaredMethod.invoke(AudioManager::class.java, "disableSafeMediaVolume", arrayOf<Class<*>>()) as Method
disableSafeMediaVolumeMethod.invoke(audioManager)
Вызов заканчивается исключением
java.lang.SecurityException: Only SystemUI can disable the safe media volume: Neither user 10307 nor current process has android.permission.STATUS_BAR_SERVICE.
Разрешение STATUS_BAR_SERVICE имеет protectionLevel=«signature|privileged», получить его не получится.
Что ж, тогда попробуем так. Мы будем следить за переменной
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS
, в которую периодически сохраняется текущее значение mMusicActiveMs
. Когда значение начнёт приближаться к 20 часам, будем его сбрасывать. Затем нужно будет сделать так, чтобы AudioService прочитал новое значение из настроек.Прочитать значение
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS
можно так:val unsafeMs = Settings.Secure.getInt(contentResolver, "unsafe_volume_music_active_ms")
То же самое, используя adb:
adb shell settings get secure unsafe_volume_music_active_ms
А чтобы записать значение, приложению потребуется разрешение
android.permission.WRITE_SECURE_SETTINGS
.Оно имеет protectionLevel=«signature|privileged|development», а значит его можно выдать приложению используя adb:
adb shell pm grant com.example.app android.permission.WRITE_SECURE_SETTINGS
Само значение записать можно так:
Settings.Secure.putInt(contentResolver, "unsafe_volume_music_active_ms"
, 1)
То же самое можно сделать с помощью adb:
adb shell settings put secure unsafe_volume_music_active_ms 1
Сбрасывать лучше в 1, как это сделано в AudioManager, а не в 0. Так как 0 соответствует состоянию ACTIVE.
Теперь нужно, чтобы AudioService прочитал новое значение, и обновил значение локальной переменной
mMusicActiveMs
.Есть подходящий метод в AudioManager.java
/**
* @hide
* Reload audio settings. This method is called by Settings backup
* agent when audio settings are restored and causes the AudioService
* to read and apply restored settings.
*/
public void reloadAudioSettings() {
…
}
Он инициирует вызов метода
readAudioSettings
в AudioService
, где происходит загрузка mMusicActiveMs
из настроек.private void readAudioSettings(boolean userSwitch) {
...
synchronized (mSafeMediaVolumeStateLock) {
mMusicActiveMs = MathUtils.constrain(Settings.Secure.getIntForUser(mContentResolver,
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, 0, UserHandle.USER_CURRENT),
0, UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX);
if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) {
enforceSafeMediaVolume(TAG);
}
}
Метод помечен аннотацией
@hide
. Его вызов с помощью double-reflection вызывает исключение:java.lang.SecurityException: Permission Denial: get/set setting for user asks to run as user -2 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL
Да, аннотация
@hide
здесь тоже неспроста. Получить данное разрешение мы, конечно не можем. Оно имеет protectionLevel=«signature|installer».Остаётся один способ заставить AudioService прочитать новое значение — его перезапуск. Просто так перезапустить системный сервис нельзя. Нужно или перезагрузить устройство, или переключиться на другого пользователя, а затем вернуться обратно.
Теперь настало время проверить теорию.
Устанавливаем
unsafe_volume_music_active_ms = 71 990 000
(останется 10 секунд, в течение которых можно прослушивать музыку на высокой громкости)adb shell settings put secure unsafe_volume_music_active_ms 71990000
Перезапускаем устройство (можно вместо этого переключиться на другого пользователя, а потом вернуться):
adb reboot
Подключаем наушники, включаем музыку погромче. В течение минуты появляется диалог.
Теперь повторяем те же действия, но присваиваем unsafe_volume_music_active_ms = 1. Включаем музыку, ждём минуту. Диалог не появляется.
Итоги
Чтобы отключить предупреждение, можно сделать следующее:
При наличии root-прав
Установить значение audio.safemedia.bypass=true в файле system/build.properties
Без root-прав
Нужно следить за значением
Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS
, и не давать ему подниматься выше 72 000 000 (20 часов). После сброса значения нужно перезапускать устройство (или переключаться на другого пользователя, а затем возвращаться обратно).Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.
Обновление
Производители устройств могут вносить изменения в код платформы, и, судя по комментариям, некоторые из них смягчают дефолтное поведение. Например, предупреждение может появиться один раз, и больше не возникать до следующей перезагрузки.