В этой статье мы рассмотрим, почему и когда возникает данное предупреждение, и как сделать так, чтобы оно больше не возникало.
Появляется оно только при прослушивании аудио через внешнее устройство (наушники/колонки). Для тех, кто не встречался с таким, небольшое пояснение: представьте, что вы слушаете музыку в наушниках, довольно громкую. Внезапно звук становится тише. Вы пытаетесь прибавить громкость, используя кнопки на корпусе, но не выходит. Достав устройство из кармана и сняв блокировку, вы и увидите такое предупреждение. Только после согласия с ним можно будет прибавить громкость обратно.
Да, предупреждение разумное, но появляется оно в непредсказуемый момент, иногда самый неподходящий: когда вы в общественном транспорте/за рулём/зимой на улице/когда у вас грязные руки и т.д. Доставать устройство, снимать блокировку, соглашаться с предупреждением, класть устройство обратно в этих случаях неудобно. А в случае, если подключены колонки, а не наушники, сообщение не совсем уместно.
Почему оно возникает
Данное предупреждение — не собственная инициатива авторов платформы. Всё дело в том, что существует 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 часов). После сброса значения нужно перезапускать устройство (или переключаться на другого пользователя, а затем возвращаться обратно).Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.
Обновление
Производители устройств могут вносить изменения в код платформы, и, судя по комментариям, некоторые из них смягчают дефолтное поведение. Например, предупреждение может появиться один раз, и не больше возникать до следующей перезагрузки.
ligor
О как меня бесила эта «фича» в старой Нокии на Винфоне 8, в самый неподходящий момент когда долго добираться до «тела». К счастью, в нынешей Нокии на Андроид 10 такого еще не происходило.