summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/res/res/values/config.xml3
-rw-r--r--core/res/res/values/strings.xml22
-rw-r--r--core/res/res/values/symbols.xml5
-rw-r--r--media/java/android/media/AudioManager.java43
-rw-r--r--media/java/android/media/IAudioService.aidl2
-rw-r--r--media/java/android/media/IVolumeController.aidl15
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java215
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/Events.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java52
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java55
-rw-r--r--services/core/java/com/android/server/audio/SoundDoseHelper.java64
13 files changed, 525 insertions, 3 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f995a6eeb5a3..6740573ff79d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2011,6 +2011,9 @@
<!-- The default volume for the ring stream -->
<integer name="config_audio_ring_vol_default">5</integer>
+ <!-- Enable sound dose computation and warnings -->
+ <bool name="config_audio_csd_enabled_default">false</bool>
+
<!-- The default value for whether head tracking for
spatial audio is enabled for a newly connected audio device -->
<bool name="config_spatial_audio_head_tracking_enabled_default">false</bool>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 8106e247ae63..73810f66a724 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4824,6 +4824,28 @@
"Raise volume above recommended level?\n\nListening at high volume for long periods may damage your hearing."
</string>
+ <!-- Message shown in dialog when user goes over a multiple of 100% of the safe weekly dose -->
+ <string name="csd_dose_reached_warning" product="default">
+ "Warning,\nYou have exceeded the amount of loud sound signals one can safely listen to in a week over headphones.\n\nGoing over this limit will permanently damage your hearing."
+ </string>
+
+ <!-- Message shown in dialog when user goes over 500% of the safe weekly dose and volume is
+ automatically lowered -->
+ <string name="csd_dose_repeat_warning" product="default">
+ "Warning,\nYou have exceeded 5 times the amount of loud sound signals one can safely listen to in a week over headphones.\n\nVolume has been lowered to protect your hearing."
+ </string>
+
+ <!-- Message shown in dialog when user's dose enters RS2 -->
+ <string name="csd_entering_RS2_warning" product="default">
+ "The level at which you are listening to media can result in hearing damage when sustained over long periods of time.\n\nContinuing to play at this level for long periods of time could damage your hearing."
+ </string>
+
+ <!-- Message shown in dialog when user is momentarily listening to unsafely loud content
+ over headphones -->
+ <string name="csd_momentary_exposure_warning" product="default">
+ "Warning,\nYou are currently listening to loud content played at an unsafe level.\n\nContinuing to listen this loud will permanently damage your hearing."
+ </string>
+
<!-- Dialog title for dialog shown when the accessibility shortcut is activated, and we want
to confirm that the user understands what's going to happen-->
<string name="accessibility_shortcut_warning_dialog_title">Use Accessibility Shortcut?</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 151530bcccfb..143ce864da32 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -283,6 +283,7 @@
<java-symbol type="attr" name="autofillSaveCustomSubtitleMaxHeight"/>
<java-symbol type="bool" name="action_bar_embed_tabs" />
<java-symbol type="bool" name="action_bar_expanded_action_views_exclusive" />
+ <java-symbol type="bool" name="config_audio_csd_enabled_default" />
<java-symbol type="integer" name="config_audio_notif_vol_default" />
<java-symbol type="integer" name="config_audio_notif_vol_steps" />
<java-symbol type="integer" name="config_audio_ring_vol_default" />
@@ -721,6 +722,10 @@
<java-symbol type="string" name="contentServiceSync" />
<java-symbol type="string" name="contentServiceSyncNotificationTitle" />
<java-symbol type="string" name="contentServiceTooManyDeletesNotificationDesc" />
+ <java-symbol type="string" name="csd_dose_reached_warning" />
+ <java-symbol type="string" name="csd_dose_repeat_warning" />
+ <java-symbol type="string" name="csd_entering_RS2_warning" />
+ <java-symbol type="string" name="csd_momentary_exposure_warning" />
<java-symbol type="string" name="date_and_time" />
<java-symbol type="string" name="date_picker_decrement_day_button" />
<java-symbol type="string" name="date_picker_decrement_month_button" />
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3ed2c4b6ebc7..b0609ec88e43 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -6237,6 +6237,49 @@ public class AudioManager {
}
/**
+ * @hide
+ * Lower media volume to RS1
+ */
+ public void lowerVolumeToRs1() {
+ try {
+ getService().lowerVolumeToRs1(mApplicationContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Sound dose warning at every 100% of dose during integration window
+ */
+ public static final int CSD_WARNING_DOSE_REACHED_1X = 1;
+ /**
+ * @hide
+ * Sound dose warning when 500% of dose is reached during integration window
+ */
+ public static final int CSD_WARNING_DOSE_REPEATED_5X = 2;
+ /**
+ * @hide
+ * Sound dose warning after a momentary exposure event
+ */
+ public static final int CSD_WARNING_MOMENTARY_EXPOSURE = 3;
+ /**
+ * @hide
+ * Sound dose warning at every 100% of dose during integration window
+ */
+ public static final int CSD_WARNING_ACCUMULATION_START = 4;
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ CSD_WARNING_DOSE_REACHED_1X,
+ CSD_WARNING_DOSE_REPEATED_5X,
+ CSD_WARNING_MOMENTARY_EXPOSURE,
+ CSD_WARNING_ACCUMULATION_START }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CsdWarning {}
+
+ /**
* Only useful for volume controllers.
* @hide
*/
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 5502db2da4c6..05a0b865dd3a 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -261,6 +261,8 @@ interface IAudioService {
void disableSafeMediaVolume(String callingPackage);
+ void lowerVolumeToRs1(String callingPackage);
+
int setHdmiSystemAudioSupported(boolean on);
boolean isHdmiSystemAudioSupported();
diff --git a/media/java/android/media/IVolumeController.aidl b/media/java/android/media/IVolumeController.aidl
index 7f372654124d..9cb2ebaaf72b 100644
--- a/media/java/android/media/IVolumeController.aidl
+++ b/media/java/android/media/IVolumeController.aidl
@@ -39,4 +39,19 @@ oneway interface IVolumeController {
* {@link VolumePolicy#A11Y_MODE_INDEPENDENT_A11Y_VOLUME}
*/
void setA11yMode(int mode);
+
+ /**
+ * Display a sound-dose related warning.
+ * This method will never be called if the CSD (Computed Sound Dose) feature is
+ * not enabled. See com.android.android.server.audio.SoundDoseHelper for the state of
+ * the feature.
+ * @param warning the type of warning to display, values are one of
+ * {@link android.media.AudioManager#CSD_WARNING_DOSE_REACHED_1X},
+ * {@link android.media.AudioManager#CSD_WARNING_DOSE_REPEATED_5X},
+ * {@link android.media.AudioManager#CSD_WARNING_MOMENTARY_EXPOSURE},
+ * {@link android.media.AudioManager#CSD_WARNING_ACCUMULATION_START}.
+ * @param displayDurationMs the time expressed in milliseconds after which the dialog will be
+ * automatically dismissed, or -1 if there is no automatic timeout.
+ */
+ void displayCsdWarning(int warning, int displayDurationMs);
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index 894bb5fc8577..cf7d2c57923c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -179,8 +179,9 @@ public interface VolumeDialogController {
@ProvidesInterface(version = Callbacks.VERSION)
public interface Callbacks {
- int VERSION = 1;
+ int VERSION = 2;
+ // requires version 1
void onShowRequested(int reason, boolean keyguardLocked, int lockTaskModeState);
void onDismissRequested(int reason);
void onStateChanged(State state);
@@ -192,5 +193,7 @@ public interface VolumeDialogController {
void onShowSafetyWarning(int flags);
void onAccessibilityModeChanged(Boolean showA11yStream);
void onCaptionComponentStateChanged(Boolean isComponentEnabled, Boolean fromTooltip);
+ // requires version 2
+ void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
new file mode 100644
index 000000000000..35af44442ca9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume;
+
+import android.annotation.StringRes;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.CountDownTimer;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+/**
+ * A class that implements the four Computed Sound Dose-related warnings defined in {@link AudioManager}:
+ * <ul>
+ * <li>{@link AudioManager#CSD_WARNING_DOSE_REACHED_1X}</li>
+ * <li>{@link AudioManager#CSD_WARNING_DOSE_REPEATED_5X}</li>
+ * <li>{@link AudioManager#CSD_WARNING_ACCUMULATION_START}</li>
+ * <li>{@link AudioManager#CSD_WARNING_MOMENTARY_EXPOSURE}</li>
+ * </ul>
+ * Rather than basing volume safety messages on a fixed volume index, the CSD feature derives its
+ * warnings from the computation of the "sound dose". The dose computation is based on a
+ * frequency-dependent analysis of the audio signal which estimates how loud and potentially harmful
+ * the signal content is. This is combined with the volume attenuation/amplification applied to it
+ * and integrated over time to derive the dose exposure over a 7 day rolling window.
+ * <p>The UI behaviors implemented in this class are defined in IEC 62368 in "Safeguards against
+ * acoustic energy sources". The events that trigger those warnings originate in SoundDoseHelper
+ * which runs in the "audio" system_server service (see
+ * frameworks/base/services/core/java/com/android/server/audio/AudioService.java for the
+ * communication between the audio framework and the volume controller, and
+ * frameworks/base/services/core/java/com/android/server/audio/SoundDoseHelper.java for the
+ * communication between the native audio framework that implements the dose computation and the
+ * audio service.
+ */
+public abstract class CsdWarningDialog extends SystemUIDialog
+ implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener {
+
+ private static final String TAG = Util.logTag(CsdWarningDialog.class);
+
+ private static final int KEY_CONFIRM_ALLOWED_AFTER_MS = 1000; // milliseconds
+ // time after which action is taken when the user hasn't ack'd or dismissed the dialog
+ private static final int NO_ACTION_TIMEOUT_MS = 5000;
+
+ private final Context mContext;
+ private final AudioManager mAudioManager;
+ private final @AudioManager.CsdWarning int mCsdWarning;
+ private final Object mTimerLock = new Object();
+ /**
+ * Timer to keep track of how long the user has before an action (here volume reduction) is
+ * taken on their behalf.
+ */
+ @GuardedBy("mTimerLock")
+ private final CountDownTimer mNoUserActionTimer;
+
+ private long mShowTime;
+
+ public CsdWarningDialog(@AudioManager.CsdWarning int csdWarning, Context context,
+ AudioManager audioManager) {
+ super(context);
+ mCsdWarning = csdWarning;
+ mContext = context;
+ mAudioManager = audioManager;
+ getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
+ setShowForAllUsers(true);
+ setMessage(mContext.getString(getStringForWarning(csdWarning)));
+ setButton(DialogInterface.BUTTON_POSITIVE,
+ mContext.getString(com.android.internal.R.string.yes), this);
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ mContext.getString(com.android.internal.R.string.no), this);
+ setOnDismissListener(this);
+
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.registerReceiver(mReceiver, filter,
+ Context.RECEIVER_EXPORTED_UNAUDITED);
+
+ if (csdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) {
+ mNoUserActionTimer = new CountDownTimer(NO_ACTION_TIMEOUT_MS, NO_ACTION_TIMEOUT_MS) {
+ @Override
+ public void onTick(long millisUntilFinished) { }
+
+ @Override
+ public void onFinish() {
+ if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) {
+ // unlike on the 5x dose repeat, level is only reduced to RS1
+ // when the warning is not acknowledged quick enough
+ mAudioManager.lowerVolumeToRs1();
+ }
+ }
+ };
+ } else {
+ mNoUserActionTimer = null;
+ }
+ }
+
+ protected abstract void cleanUp();
+
+ // NOT overriding onKeyDown as we're not allowing a dismissal on any key other than
+ // VOLUME_DOWN, and for this, we don't need to track if it's the start of a new
+ // key down -> up sequence
+ //@Override
+ //public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // return super.onKeyDown(keyCode, event);
+ //}
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // never allow to raise volume
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ return true;
+ }
+ // VOLUME_DOWN will dismiss the dialog
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ && (System.currentTimeMillis() - mShowTime) > KEY_CONFIRM_ALLOWED_AFTER_MS) {
+ Log.i(TAG, "Confirmed CSD exposure warning via VOLUME_DOWN");
+ dismiss();
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ Log.d(TAG, "OK pressed for CSD warning " + mCsdWarning);
+ dismiss();
+
+ }
+ if (D.BUG) Log.d(TAG, "on click " + which);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mShowTime = System.currentTimeMillis();
+ synchronized (mTimerLock) {
+ if (mNoUserActionTimer != null) {
+ new Thread(() -> {
+ synchronized (mTimerLock) {
+ mNoUserActionTimer.start();
+ }
+ }).start();
+ }
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ synchronized (mTimerLock) {
+ if (mNoUserActionTimer != null) {
+ mNoUserActionTimer.cancel();
+ }
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface unused) {
+ if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REPEATED_5X) {
+ // level is always reduced to RS1 beyond the 5x dose
+ mAudioManager.lowerVolumeToRs1();
+ }
+ try {
+ mContext.unregisterReceiver(mReceiver);
+ } catch (IllegalArgumentException e) {
+ // Don't crash if the receiver has already been unregistered.
+ Log.e(TAG, "Error unregistering broadcast receiver", e);
+ }
+ cleanUp();
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ if (D.BUG) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
+ cancel();
+ cleanUp();
+ }
+ }
+ };
+
+ private @StringRes int getStringForWarning(@AudioManager.CsdWarning int csdWarning) {
+ switch (csdWarning) {
+ case AudioManager.CSD_WARNING_DOSE_REACHED_1X:
+ return com.android.internal.R.string.csd_dose_reached_warning;
+ case AudioManager.CSD_WARNING_DOSE_REPEATED_5X:
+ return com.android.internal.R.string.csd_dose_repeat_warning;
+ case AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE:
+ return com.android.internal.R.string.csd_momentary_exposure_warning;
+ case AudioManager.CSD_WARNING_ACCUMULATION_START:
+ return com.android.internal.R.string.csd_entering_RS2_warning;
+ }
+ Log.e(TAG, "Invalid CSD warning event " + csdWarning, new Exception());
+ return com.android.internal.R.string.csd_dose_reached_warning;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/Events.java b/packages/SystemUI/src/com/android/systemui/volume/Events.java
index 369552fc814d..fc0033d71844 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/Events.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/Events.java
@@ -97,6 +97,7 @@ public class Events {
public static final int DISMISS_STREAM_GONE = 7;
public static final int DISMISS_REASON_OUTPUT_CHOOSER = 8;
public static final int DISMISS_REASON_USB_OVERHEAD_ALARM_CHANGED = 9;
+ public static final int DISMISS_REASON_CSD_WARNING_TIMEOUT = 10;
public static final String[] DISMISS_REASONS = {
"unknown",
"touch_outside",
@@ -107,7 +108,8 @@ public class Events {
"done_clicked",
"a11y_stream_changed",
"output_chooser",
- "usb_temperature_below_threshold"
+ "usb_temperature_below_threshold",
+ "csd_warning_timeout"
};
public static final int SHOW_REASON_UNKNOWN = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 934996633f50..2fc8b03f7771 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -410,6 +410,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
}
+ private void onShowCsdWarningW(@AudioManager.CsdWarning int csdWarning, int durationMs) {
+ mCallbacks.onShowCsdWarning(csdWarning, durationMs);
+ }
+
private void onGetCaptionsComponentStateW(boolean fromTooltip) {
mCallbacks.onCaptionComponentStateChanged(
mCaptioningManager.isSystemAudioCaptioningUiEnabled(), fromTooltip);
@@ -707,6 +711,27 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
mWorker.obtainMessage(W.SHOW_SAFETY_WARNING, flags, 0).sendToTarget();
}
+ /**
+ * Display a sound-dose related warning.
+ * This method will never be called if the CSD (Computed Sound Dose) feature is
+ * not enabled. See com.android.android.server.audio.SoundDoseHelper for the state of
+ * the feature.
+ * @param warning the type of warning to display, values are one of
+ * {@link android.media.AudioManager#CSD_WARNING_DOSE_REACHED_1X},
+ * {@link android.media.AudioManager#CSD_WARNING_DOSE_REPEATED_5X},
+ * {@link android.media.AudioManager#CSD_WARNING_MOMENTARY_EXPOSURE},
+ * {@link android.media.AudioManager#CSD_WARNING_ACCUMULATION_START}.
+ * @param displayDurationMs the time expressed in milliseconds after which the dialog will be
+ * automatically dismissed, or -1 if there is no automatic timeout.
+ */
+ @Override
+ public void displayCsdWarning(int csdWarning, int displayDurationMs) throws RemoteException
+ {
+ if (D.BUG) Log.d(TAG, "displayCsdWarning durMs=" + displayDurationMs);
+ mWorker.obtainMessage(W.SHOW_CSD_WARNING, csdWarning, displayDurationMs)
+ .sendToTarget();
+ }
+
@Override
public void volumeChanged(int streamType, int flags) throws RemoteException {
if (D.BUG) Log.d(TAG, "volumeChanged " + AudioSystem.streamToString(streamType)
@@ -769,6 +794,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
private static final int SHOW_SAFETY_WARNING = 14;
private static final int ACCESSIBILITY_MODE_CHANGED = 15;
private static final int GET_CAPTIONS_COMPONENT_STATE = 16;
+ private static final int SHOW_CSD_WARNING = 17;
W(Looper looper) {
super(looper);
@@ -794,6 +820,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
case GET_CAPTIONS_COMPONENT_STATE:
onGetCaptionsComponentStateW((Boolean) msg.obj); break;
case ACCESSIBILITY_MODE_CHANGED: onAccessibilityModeChanged((Boolean) msg.obj);
+ break;
+ case SHOW_CSD_WARNING: onShowCsdWarningW(msg.arg1, msg.arg2); break;
}
}
}
@@ -925,6 +953,21 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
@Override
+ public void onShowCsdWarning(int csdWarning, int durationMs) {
+ if (Callbacks.VERSION < 2) {
+ return;
+ }
+ for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+ entry.getValue().post(new Runnable() {
+ @Override
+ public void run() {
+ entry.getKey().onShowCsdWarning(csdWarning, durationMs);
+ }
+ });
+ }
+ }
+
+ @Override
public void onAccessibilityModeChanged(Boolean showA11yStream) {
boolean show = showA11yStream != null && showA11yStream;
for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index fa3c73a26f7b..01b21e2fa562 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -109,6 +109,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
@@ -272,7 +273,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE;
private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
private State mState;
+ @GuardedBy("mSafetyWarningLock")
private SafetyWarningDialog mSafetyWarning;
+ @GuardedBy("mSafetyWarningLock")
+ private CsdWarningDialog mCsdDialog;
private boolean mHovering = false;
private final boolean mShowActiveStreamOnly;
private boolean mConfigChanged = false;
@@ -1464,6 +1468,23 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
AccessibilityManager.FLAG_CONTENT_CONTROLS);
}
+ protected void scheduleCsdTimeoutH(int timeoutMs) {
+ mHandler.removeMessages(H.CSD_TIMEOUT);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(H.CSD_TIMEOUT,
+ Events.DISMISS_REASON_CSD_WARNING_TIMEOUT, 0), timeoutMs);
+ Log.i(TAG, "scheduleCsdTimeoutH " + timeoutMs + "ms " + Debug.getCaller());
+ mController.userActivity();
+ }
+
+ private void onCsdTimeoutH() {
+ synchronized (mSafetyWarningLock) {
+ if (mCsdDialog == null) {
+ return;
+ }
+ mCsdDialog.dismiss();
+ }
+ }
+
protected void dismissH(int reason) {
Trace.beginSection("VolumeDialogImpl#dismissH");
@@ -2045,6 +2066,30 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
rescheduleTimeoutH();
}
+ private void showCsdWarningH(int csdWarning, int durationMs) {
+ synchronized (mSafetyWarningLock) {
+ if (mCsdDialog != null) {
+ return;
+ }
+ mCsdDialog = new CsdWarningDialog(csdWarning,
+ mContext, mController.getAudioManager()) {
+ @Override
+ protected void cleanUp() {
+ synchronized (mSafetyWarningLock) {
+ mCsdDialog = null;
+ }
+ recheckH(null);
+ }
+ };
+ mCsdDialog.show();
+ }
+ recheckH(null);
+ if (durationMs > 0) {
+ scheduleCsdTimeoutH(durationMs);
+ }
+ rescheduleTimeoutH();
+ }
+
private String getStreamLabelH(StreamState ss) {
if (ss == null) {
return "";
@@ -2224,6 +2269,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
}
@Override
+ public void onShowCsdWarning(int csdWarning, int durationMs) {
+ showCsdWarningH(csdWarning, durationMs);
+ }
+
+ @Override
public void onAccessibilityModeChanged(Boolean showA11yStream) {
mShowA11yStream = showA11yStream == null ? false : showA11yStream;
VolumeRow activeRow = getActiveRow();
@@ -2250,6 +2300,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
private static final int SET_STREAM_IMPORTANT = 5;
private static final int RESCHEDULE_TIMEOUT = 6;
private static final int STATE_CHANGED = 7;
+ private static final int CSD_TIMEOUT = 8;
public H() {
super(Looper.getMainLooper());
@@ -2266,6 +2317,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break;
case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break;
case STATE_CHANGED: onStateChangedH(mState); break;
+ case CSD_TIMEOUT: onCsdTimeoutH(); break;
}
}
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index fe40bd60bc01..568daf733655 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -21,6 +21,7 @@ import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT;
import static android.media.AudioManager.RINGER_MODE_NORMAL;
import static android.media.AudioManager.RINGER_MODE_SILENT;
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
+import static android.media.AudioManager.STREAM_MUSIC;
import static android.media.AudioManager.STREAM_SYSTEM;
import static android.os.Process.FIRST_APPLICATION_UID;
import static android.os.Process.INVALID_UID;
@@ -382,6 +383,7 @@ public class AudioService extends IAudioService.Stub
private static final int MSG_RESET_SPATIALIZER = 50;
private static final int MSG_NO_LOG_FOR_PLAYER_I = 51;
private static final int MSG_DISPATCH_PREFERRED_MIXER_ATTRIBUTES = 52;
+ private static final int MSG_LOWER_VOLUME_TO_RS1 = 53;
/** Messages handled by the {@link SoundDoseHelper}. */
/*package*/ static final int SAFE_MEDIA_VOLUME_MSG_START = 1000;
@@ -8580,6 +8582,10 @@ public class AudioService extends IAudioService.Stub
onDispatchPreferredMixerAttributesChanged(msg.getData(), msg.arg1);
break;
+ case MSG_LOWER_VOLUME_TO_RS1:
+ onLowerVolumeToRs1();
+ break;
+
default:
if (msg.what >= SAFE_MEDIA_VOLUME_MSG_START) {
// msg could be for the SoundDoseHelper
@@ -9787,6 +9793,38 @@ public class AudioService extends IAudioService.Stub
mSoundDoseHelper.disableSafeMediaVolume(callingPackage);
}
+ @Override
+ public void lowerVolumeToRs1(String callingPackage) {
+ enforceVolumeController("lowerVolumeToRs1");
+ postLowerVolumeToRs1();
+ }
+
+ /*package*/ void postLowerVolumeToRs1() {
+ sendMsg(mAudioHandler, MSG_LOWER_VOLUME_TO_RS1, SENDMSG_QUEUE,
+ // no params, no delay
+ 0, 0, null, 0);
+ }
+
+ /**
+ * Called when handling MSG_LOWER_VOLUME_TO_RS1
+ */
+ private void onLowerVolumeToRs1() {
+ final ArrayList<AudioDeviceAttributes> devices = getDevicesForAttributesInt(
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(), true);
+ final int nativeDeviceType;
+ final AudioDeviceAttributes ada;
+ if (devices.isEmpty()) {
+ ada = devices.get(0);
+ nativeDeviceType = ada.getInternalType();
+ } else {
+ nativeDeviceType = AudioSystem.DEVICE_OUT_USB_HEADSET;
+ ada = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_USB_HEADSET, "");
+ }
+ final int index = mSoundDoseHelper.safeMediaVolumeIndex(nativeDeviceType);
+ setStreamVolumeWithAttributionInt(STREAM_MUSIC, index, /*flags*/ 0, ada,
+ "com.android.server.audio", "AudioService");
+ }
+
//==========================================================================================
// Hdmi CEC:
// - System audio mode:
@@ -10298,6 +10336,9 @@ public class AudioService extends IAudioService.Stub
public interface ISafeHearingVolumeController {
/** Displays an instructional safeguard as required by the safe hearing standard. */
void postDisplaySafeVolumeWarning(int flags);
+
+ /** Displays a warning about transient exposure to high level playback */
+ void postDisplayCsdWarning(@AudioManager.CsdWarning int csdEvent, int displayDurationMs);
}
/** Wrapper which encapsulates the {@link IVolumeController} functionality. */
@@ -10400,6 +10441,20 @@ public class AudioService extends IAudioService.Stub
}
}
+ @Override
+ public void postDisplayCsdWarning(
+ @AudioManager.CsdWarning int csdWarning, int displayDurationMs) {
+ if (mController == null) {
+ Log.e(TAG, "Unable to display CSD warning, no controller");
+ return;
+ }
+ try {
+ mController.displayCsdWarning(csdWarning, displayDurationMs);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error calling displayCsdWarning for warning " + csdWarning, e);
+ }
+ }
+
public void postVolumeChanged(int streamType, int flags) {
if (mController == null)
return;
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 5fe9ada8c9cc..16a0149fe94c 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -26,6 +26,7 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.ISoundDose;
import android.media.ISoundDoseCallback;
@@ -40,6 +41,7 @@ import android.provider.Settings;
import android.util.Log;
import android.util.MathUtils;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.server.audio.AudioService.AudioHandler;
import com.android.server.audio.AudioService.ISafeHearingVolumeController;
@@ -96,11 +98,19 @@ public class SoundDoseHelper {
private static final float CUSTOM_RS2_VALUE = 90;
+ // timeouts for the CSD warnings, -1 means no timeout (dialog must be ack'd by user)
+ private static final int CSD_WARNING_TIMEOUT_MS_DOSE_1X = 7000;
+ private static final int CSD_WARNING_TIMEOUT_MS_DOSE_5X = 5000;
+ private static final int CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START = -1;
+ private static final int CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE = 5000;
+
private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
"CSD updates");
private int mMcc = 0;
+ private final boolean mEnableCsd;
+
final Object mSafeMediaVolumeStateLock = new Object();
private int mSafeMediaVolumeState;
@@ -144,6 +154,8 @@ public class SoundDoseHelper {
private ISoundDose mSoundDose;
private float mCurrentCsd = 0.f;
+ // dose at which the next dose reached warning occurs
+ private float mNextCsdWarning = 1.0f;
private final List<SoundDoseRecord> mDoseRecords = new ArrayList<>();
private final Context mContext;
@@ -153,10 +165,42 @@ public class SoundDoseHelper {
Log.w(TAG, "DeviceId " + deviceId + " triggered momentary exposure with value: "
+ currentMel);
mLogger.enqueue(SoundDoseEvent.getMomentaryExposureEvent(currentMel));
+ if (mEnableCsd) {
+ mVolumeController.postDisplayCsdWarning(
+ AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE,
+ getTimeoutMsForWarning(AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE));
+ }
}
public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) {
Log.i(TAG, "onNewCsdValue: " + currentCsd);
+ if (mCurrentCsd < currentCsd) {
+ // dose increase: going over next threshold?
+ if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
+ if (mEnableCsd) {
+ if (mNextCsdWarning == 5.0f) {
+ // 500% dose repeat
+ mVolumeController.postDisplayCsdWarning(
+ AudioManager.CSD_WARNING_DOSE_REPEATED_5X,
+ getTimeoutMsForWarning(
+ AudioManager.CSD_WARNING_DOSE_REPEATED_5X));
+ // on the 5x dose warning, the volume reduction happens right away
+ mAudioService.postLowerVolumeToRs1();
+ } else {
+ mVolumeController.postDisplayCsdWarning(
+ AudioManager.CSD_WARNING_DOSE_REACHED_1X,
+ getTimeoutMsForWarning(
+ AudioManager.CSD_WARNING_DOSE_REACHED_1X));
+ }
+ }
+ mNextCsdWarning += 1.0f;
+ }
+ } else {
+ // dose decrease: dropping below previous threshold of warning?
+ if ((currentCsd < mNextCsdWarning - 1.0f) && (mNextCsdWarning >= 2.0f)) {
+ mNextCsdWarning -= 1.0f;
+ }
+ }
mCurrentCsd = currentCsd;
mDoseRecords.addAll(Arrays.asList(records));
long totalDuration = 0;
@@ -184,10 +228,12 @@ public class SoundDoseHelper {
mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
+ mEnableCsd = mContext.getResources().getBoolean(R.bool.config_audio_csd_enabled_default);
+
// The default safe volume index read here will be replaced by the actual value when
// the mcc is read by onConfigureSafeVolume()
mSafeMediaVolumeIndex = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_safe_media_volume_index) * 10;
+ R.integer.config_safe_media_volume_index) * 10;
mAlarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
@@ -401,6 +447,7 @@ public class SoundDoseHelper {
}
/*package*/ void dump(PrintWriter pw) {
+ pw.print(" mEnableCsd="); pw.println(mEnableCsd);
pw.print(" mSafeMediaVolumeState=");
pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState));
pw.print(" mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex);
@@ -491,6 +538,21 @@ public class SoundDoseHelper {
}
}
+ private int getTimeoutMsForWarning(@AudioManager.CsdWarning int csdWarning) {
+ switch (csdWarning) {
+ case AudioManager.CSD_WARNING_DOSE_REACHED_1X:
+ return CSD_WARNING_TIMEOUT_MS_DOSE_1X;
+ case AudioManager.CSD_WARNING_DOSE_REPEATED_5X:
+ return CSD_WARNING_TIMEOUT_MS_DOSE_5X;
+ case AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE:
+ return CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE;
+ case AudioManager.CSD_WARNING_ACCUMULATION_START:
+ return CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START;
+ }
+ Log.e(TAG, "Invalid CSD warning " + csdWarning, new Exception());
+ return -1;
+ }
+
@GuardedBy("mSafeMediaVolumeStateLock")
private void setSafeMediaVolumeEnabled(boolean on, String caller) {
if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) && (mSafeMediaVolumeState