
В этой статье мы рассмотрим, почему и когда возникает данное предупреждение, и как сделать так, чтобы оно больше не возникало.
Появляется оно только при прослушивании аудио через внешнее устройство (наушники/колонки). Для тех, кто не встречался с таким, небольшое пояснение: представьте, что вы слушаете музыку в наушниках, довольно громкую. Внезапно звук становится тише. Вы пытаетесь прибавить громкость, используя кнопки на корпусе, но не выходит. Достав устройство из кармана и сняв блокировку, вы и увидите такое предупреждение. Только после согласия с ним можно будет прибавить громкость обратно.
Да, предупреждение разумное, но появляется оно в непредсказуемый момент, иногда самый неподходящий: когда вы в общественном транспорте/за рулём/зимой на улице/когда у вас грязные руки и т.д. Доставать устройство, снимать блокировку, соглашаться с предупреждением, класть устройство обратно в этих случаях неудобно. А в случае, если подключены колонки, а не наушники, сообщение не совсем уместно.
Почему оно возникает
Данное предупреждение — не собственная инициатива авторов платформы. Всё дело в том, что существует 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 часов). После сброса значения нужно перезапускать устройство (или переключаться на другого пользователя, а затем возвращаться обратно).
Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.
ссылка на оригинал статьи https://habr.com/ru/post/506870/
Добавить комментарий