Русский
Русский
English
Статистика
Реклама

Из песочницы Как отключить предупреждение о вреде долгого прослушивания аудио (Android)

Наверное, многие, кто слушает музыку (и не только) с 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 hoursprivate 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;  // confirmedprivate static final int SAFE_MEDIA_VOLUME_ACTIVE = 3;  // unconfirmedprivate 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 AudioManagerval getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String::class.java, arrayOf<Class<*>>()::class.java)val disableSafeMediaVolumeMethod = getDeclaredMethod.invoke(AudioManager::class.java, "disableSafeMediaVolume", arrayOf<Class<*>>()) as MethoddisableSafeMediaVolumeMethod.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 часов). После сброса значения нужно перезапускать устройство (или переключаться на другого пользователя, а затем возвращаться обратно).

Я написала код простого приложения, которое делает эту работу, и напоминает о необходимости перезагрузить устройство/перелогиниться.
Источник: habr.com
К списку статей
Опубликовано: 16.06.2020 12:17:10
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка под android

Android

Android internals

Звук

Категории

Последние комментарии

© 2006-2020, personeltest.ru