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


В этой статье мы рассмотрим, почему и когда возникает данное предупреждение, и как сделать так, чтобы оно больше не возникало.

Появляется оно только при прослушивании аудио через внешнее устройство (наушники/колонки). Для тех, кто не встречался с таким, небольшое пояснение: представьте, что вы слушаете музыку в наушниках, довольно громкую. Внезапно звук становится тише. Вы пытаетесь прибавить громкость, используя кнопки на корпусе, но не выходит. Достав устройство из кармана и сняв блокировку, вы и увидите такое предупреждение. Только после согласия с ним можно будет прибавить громкость обратно.

Да, предупреждение разумное, но появляется оно в непредсказуемый момент, иногда самый неподходящий: когда вы в общественном транспорте/за рулём/зимой на улице/когда у вас грязные руки и т.д. Доставать устройство, снимать блокировку, соглашаться с предупреждением, класть устройство обратно в этих случаях неудобно. А в случае, если подключены колонки, а не наушники, сообщение не совсем уместно.

Почему оно возникает


Данное предупреждение — не собственная инициатива авторов платформы. Всё дело в том, что существует 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 часов). После сброса значения нужно перезапускать устройство (или переключаться на другого пользователя, а затем возвращаться обратно).

Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.

Обновление


Производители устройств могут вносить изменения в код платформы, и, судя по комментариям, некоторые из них смягчают дефолтное поведение. Например, предупреждение может появиться один раз, и не больше возникать до следующей перезагрузки.