Extract logic for media safe hearing.

The SoundDoseHelper is responsible to select between the old standard
and the new sound dose approach. Extracted the previous logic for
showing a warning when the safe hearing level is exceeded into the
SoundDoseHelper

Test: manual and dumpsys audio
Bug: 257238734
Change-Id: I0af5c827f805ac9862693422399c49d1bf66fd73
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index aa8ee3d..21030a8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -27,6 +27,7 @@
 import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
 import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
 
+import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
 import static com.android.server.utils.EventLogger.Event.ALOGW;
@@ -42,12 +43,10 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityThread;
-import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.IUidObserver;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
 import android.bluetooth.BluetoothAdapter;
@@ -166,7 +165,6 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Log;
-import android.util.MathUtils;
 import android.util.PrintWriterPrinter;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -332,7 +330,7 @@
     private static final int SENDMSG_QUEUE = 2;
 
     // AudioHandler messages
-    private static final int MSG_SET_DEVICE_VOLUME = 0;
+    /*package*/ static final int MSG_SET_DEVICE_VOLUME = 0;
     private static final int MSG_PERSIST_VOLUME = 1;
     private static final int MSG_PERSIST_VOLUME_GROUP = 2;
     private static final int MSG_PERSIST_RINGER_MODE = 3;
@@ -342,13 +340,8 @@
     private static final int MSG_SET_FORCE_USE = 8;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
     private static final int MSG_SET_ALL_VOLUMES = 10;
-    private static final int MSG_CHECK_MUSIC_ACTIVE = 11;
-    private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = 12;
-    private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED = 13;
-    private static final int MSG_PERSIST_SAFE_VOLUME_STATE = 14;
     private static final int MSG_UNLOAD_SOUND_EFFECTS = 15;
     private static final int MSG_SYSTEM_READY = 16;
-    private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = 17;
     private static final int MSG_UNMUTE_STREAM = 18;
     private static final int MSG_DYN_POLICY_MIX_STATE_UPDATE = 19;
     private static final int MSG_INDICATE_SYSTEM_READY = 20;
@@ -384,6 +377,9 @@
     private static final int MSG_RESET_SPATIALIZER = 50;
     private static final int MSG_NO_LOG_FOR_PLAYER_I = 51;
 
+    /** Messages handled by the {@link SoundDoseHelper}. */
+    /*package*/ static final int SAFE_MEDIA_VOLUME_MSG_START = 1000;
+
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
     //   and not with sendMsg(..., ..., SENDMSG_QUEUE, ...)
@@ -399,6 +395,9 @@
     // List of empty UIDs used to reset the active assistant list
     private static final int[] NO_ACTIVE_ASSISTANT_SERVICE_UIDS = new int[0];
 
+    // check playback or record activity every 6 seconds for UIDs owning mode IN_COMMUNICATION
+    private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
+
     /** @see AudioSystemThread */
     private AudioSystemThread mAudioSystemThread;
     /** @see AudioHandler */
@@ -410,6 +409,10 @@
         return mStreamStates[stream].getIndex(device);
     }
 
+    /*package*/ VolumeStreamState getVssVolumeForStream(int stream) {
+        return mStreamStates[stream];
+    }
+
     /*package*/ int getMaxVssVolumeForStream(int stream) {
         return mStreamStates[stream].getMaxIndex();
     }
@@ -838,11 +841,6 @@
 
     private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
 
-    // Used when safe volume warning message display is requested by setStreamVolume(). In this
-    // case, the new requested volume, stream type and device are stored in mPendingVolumeCommand
-    // and used later when/if disableSafeMediaVolume() is called.
-    private StreamVolumeCommand mPendingVolumeCommand;
-
     private PowerManager.WakeLock mAudioEventWakeLock;
 
     private final MediaFocusControl mMediaFocusControl;
@@ -894,6 +892,8 @@
     private boolean mNavigationRepeatSoundEffectsEnabled;
     private boolean mHomeSoundEffectEnabled;
 
+    private final SoundDoseHelper mSoundDoseHelper;
+
     @GuardedBy("mSettingsLock")
     private int mCurrentImeUid;
 
@@ -1184,6 +1184,9 @@
             mAudioHandler = new AudioHandler(looper);
         }
 
+        mSoundDoseHelper = new SoundDoseHelper(this, mContext, mAudioHandler, mSettings,
+                mVolumeController);
+
         AudioSystem.setErrorCallback(mAudioSystemCallback);
 
         updateAudioHalPids();
@@ -1199,16 +1202,6 @@
                 new String("AudioService ctor"),
                 0);
 
-        mSafeMediaVolumeState = mSettings.getGlobalInt(mContentResolver,
-                                            Settings.Global.AUDIO_SAFE_VOLUME_STATE,
-                                            SAFE_MEDIA_VOLUME_NOT_CONFIGURED);
-        // 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;
-
-        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
-
         mUseFixedVolume = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useFixedVolume);
 
@@ -1285,9 +1278,7 @@
         // persistent data
         initVolumeGroupStates();
 
-        // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it
-        // relies on audio policy having correct ranges for volume indexes.
-        mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+        mSoundDoseHelper.initSafeUsbMediaVolumeIndex();
 
         // Call setRingerModeInt() to apply correct mute
         // state on streams affected by ringer mode.
@@ -1442,14 +1433,7 @@
 
         mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 
-        sendMsg(mAudioHandler,
-                MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED,
-                SENDMSG_REPLACE,
-                0,
-                0,
-                TAG,
-                SystemProperties.getBoolean("audio.safemedia.bypass", false) ?
-                        0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
+        mSoundDoseHelper.configureSafeMediaVolume(/*forced=*/true, TAG);
 
         initA11yMonitoring();
 
@@ -1987,8 +1971,8 @@
             @AudioService.ConnectionState int state, String caller) {
         if (state == AudioService.CONNECTION_STATE_CONNECTED) {
             // DEVICE_OUT_HDMI is now connected
-            if (mSafeMediaVolumeDevices.contains(AudioSystem.DEVICE_OUT_HDMI)) {
-                scheduleMusicActiveCheck();
+            if (mSoundDoseHelper.safeDevicesContains(AudioSystem.DEVICE_OUT_HDMI)) {
+                mSoundDoseHelper.scheduleMusicActiveCheck();
             }
 
             if (isPlatformTelevision()) {
@@ -3283,10 +3267,7 @@
             return;
         }
 
-        // reset any pending volume command
-        synchronized (mSafeMediaVolumeStateLock) {
-            mPendingVolumeCommand = null;
-        }
+        mSoundDoseHelper.invalidatPendingVolumeCommand();
 
         flags &= ~AudioManager.FLAG_FIXED_VOLUME;
         if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
@@ -3295,10 +3276,8 @@
             // Always toggle between max safe volume and 0 for fixed volume devices where safe
             // volume is enforced, and max and 0 for the others.
             // This is simulated by stepping by the full allowed volume range
-            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
-                    mSafeMediaVolumeDevices.contains(device)) {
-                step = safeMediaVolumeIndex(device);
-            } else {
+            step = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+            if (step < 0) {
                 step = streamState.getMaxIndex();
             }
             if (aliasIndex != 0) {
@@ -3371,10 +3350,10 @@
                         }
                     }
                 }
-            } else if ((direction == AudioManager.ADJUST_RAISE) &&
-                    !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
+            } else if ((direction == AudioManager.ADJUST_RAISE)
+                    && mSoundDoseHelper.raiseVolumeDisplaySafeMediaVolume(streamTypeAlias,
+                            aliasIndex + step, device, flags)) {
                 Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
-                mVolumeController.postDisplaySafeVolumeWarning(flags);
             } else if (!isFullVolumeDevice(device)
                     && (streamState.adjustIndex(direction * step, device, caller,
                             hasModifyAudioSettings)
@@ -3545,29 +3524,6 @@
         Binder.restoreCallingIdentity(identity);
     }
 
-    // StreamVolumeCommand contains the information needed to defer the process of
-    // setStreamVolume() in case the user has to acknowledge the safe volume warning message.
-    static class StreamVolumeCommand {
-        public final int mStreamType;
-        public final int mIndex;
-        public final int mFlags;
-        public final int mDevice;
-
-        StreamVolumeCommand(int streamType, int index, int flags, int device) {
-            mStreamType = streamType;
-            mIndex = index;
-            mFlags = flags;
-            mDevice = device;
-        }
-
-        @Override
-        public String toString() {
-            return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=")
-                    .append(mIndex).append(",flags=").append(mFlags).append(",device=")
-                    .append(mDevice).append('}').toString();
-        }
-    }
-
     private int getNewRingerMode(int stream, int index, int flags) {
         // setRingerMode does nothing if the device is single volume,so the value would be unchanged
         if (mIsSingleVolume) {
@@ -3615,7 +3571,7 @@
         return false;
     }
 
-    private void onSetStreamVolume(int streamType, int index, int flags, int device,
+     /*package*/ void onSetStreamVolume(int streamType, int index, int flags, int device,
             String caller, boolean hasModifyAudioSettings) {
         final int stream = mStreamVolumeAlias[streamType];
         setStreamVolumeInt(stream, index, device, false, caller, hasModifyAudioSettings);
@@ -3959,7 +3915,7 @@
             updateHearingAidVolumeOnVoiceActivityUpdate();
         }
         if (mMediaPlaybackActive.getAndSet(mediaActive) != mediaActive && mediaActive) {
-            scheduleMusicActiveCheck();
+            mSoundDoseHelper.scheduleMusicActiveCheck();
         }
         // Update playback active state for all apps in audio mode stack.
         // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
@@ -4205,71 +4161,64 @@
             return;
         }
 
-        synchronized (mSafeMediaVolumeStateLock) {
-            // reset any pending volume command
-            mPendingVolumeCommand = null;
+        mSoundDoseHelper.invalidatPendingVolumeCommand();
 
-            oldIndex = streamState.getIndex(device);
+        oldIndex = streamState.getIndex(device);
 
-            index = rescaleIndex(index * 10, streamType, streamTypeAlias);
+        index = rescaleIndex(index * 10, streamType, streamTypeAlias);
 
-            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
-                    && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
-                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
-                if (DEBUG_VOL) {
-                    Log.d(TAG, "setStreamVolume postSetAvrcpAbsoluteVolumeIndex index=" + index
-                            + "stream=" + streamType);
+        if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+                && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
+                && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+            if (DEBUG_VOL) {
+                Log.d(TAG, "setStreamVolume postSetAvrcpAbsoluteVolumeIndex index=" + index
+                        + "stream=" + streamType);
+            }
+            mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(index / 10);
+        } else if (isAbsoluteVolumeDevice(device)
+                && ((flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0)) {
+            AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);
+
+            dispatchAbsoluteVolumeChanged(streamType, info, index);
+        }
+
+        if (AudioSystem.isLeAudioDeviceType(device)
+                && streamType == getBluetoothContextualVolumeStream()
+                && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
+            if (DEBUG_VOL) {
+                Log.d(TAG, "adjustSreamVolume postSetLeAudioVolumeIndex index="
+                        + index + " stream=" + streamType);
+            }
+            mDeviceBroker.postSetLeAudioVolumeIndex(index, mStreamStates[streamType].getMaxIndex(),
+                    streamType);
+        }
+
+        if (device == AudioSystem.DEVICE_OUT_HEARING_AID
+                && streamType == getBluetoothContextualVolumeStream()) {
+            Log.i(TAG, "setStreamVolume postSetHearingAidVolumeIndex index=" + index
+                    + " stream=" + streamType);
+            mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType);
+        }
+
+        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+        if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+            flags |= AudioManager.FLAG_FIXED_VOLUME;
+
+            // volume is either 0 or max allowed for fixed volume devices
+            if (index != 0) {
+                index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+                if (index < 0) {
+                    index = streamState.getMaxIndex();
                 }
-                mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(index / 10);
-            } else if (isAbsoluteVolumeDevice(device)
-                    && ((flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0)) {
-                AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);
-
-                dispatchAbsoluteVolumeChanged(streamType, info, index);
-            }
-
-            if (AudioSystem.isLeAudioDeviceType(device)
-                    && streamType == getBluetoothContextualVolumeStream()
-                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
-                if (DEBUG_VOL) {
-                    Log.d(TAG, "adjustSreamVolume postSetLeAudioVolumeIndex index="
-                            + index + " stream=" + streamType);
-                }
-                mDeviceBroker.postSetLeAudioVolumeIndex(index,
-                    mStreamStates[streamType].getMaxIndex(), streamType);
-            }
-
-            if (device == AudioSystem.DEVICE_OUT_HEARING_AID
-                    && streamType == getBluetoothContextualVolumeStream()) {
-                Log.i(TAG, "setStreamVolume postSetHearingAidVolumeIndex index=" + index
-                        + " stream=" + streamType);
-                mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType);
-            }
-
-            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
-            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
-                flags |= AudioManager.FLAG_FIXED_VOLUME;
-
-                // volume is either 0 or max allowed for fixed volume devices
-                if (index != 0) {
-                    if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
-                            mSafeMediaVolumeDevices.contains(device)) {
-                        index = safeMediaVolumeIndex(device);
-                    } else {
-                        index = streamState.getMaxIndex();
-                    }
-                }
-            }
-
-            if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
-                mVolumeController.postDisplaySafeVolumeWarning(flags);
-                mPendingVolumeCommand = new StreamVolumeCommand(
-                                                    streamType, index, flags, device);
-            } else {
-                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings);
-                index = mStreamStates[streamType].getIndex(device);
             }
         }
+
+        if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
+                flags)) {
+            onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings);
+            index = mStreamStates[streamType].getIndex(device);
+        }
+
         synchronized (mHdmiClientLock) {
             if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                     && (oldIndex != index)) {
@@ -5846,14 +5795,8 @@
         checkAllAliasStreamVolumes();
         checkMuteAffectedStreams();
 
-        synchronized (mSafeMediaVolumeStateLock) {
-            mMusicActiveMs = MathUtils.constrain(mSettings.getSecureIntForUser(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);
-            }
-        }
+        mSoundDoseHelper.restoreMusicActiveMs();
+        mSoundDoseHelper.enforceSafeMediaVolumeIfActive(TAG);
 
         readVolumeGroupsSettings();
 
@@ -6189,139 +6132,6 @@
         return mContentResolver;
     }
 
-    private void scheduleMusicActiveCheck() {
-        synchronized (mSafeMediaVolumeStateLock) {
-            cancelMusicActiveCheck();
-            mMusicActiveIntent = PendingIntent.getBroadcast(mContext,
-                REQUEST_CODE_CHECK_MUSIC_ACTIVE,
-                new Intent(ACTION_CHECK_MUSIC_ACTIVE),
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                    SystemClock.elapsedRealtime()
-                    + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent);
-        }
-    }
-
-    private void cancelMusicActiveCheck() {
-        synchronized (mSafeMediaVolumeStateLock) {
-            if (mMusicActiveIntent != null) {
-                mAlarmManager.cancel(mMusicActiveIntent);
-                mMusicActiveIntent = null;
-            }
-        }
-    }
-    private void onCheckMusicActive(String caller) {
-        synchronized (mSafeMediaVolumeStateLock) {
-            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
-                int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
-                if (mSafeMediaVolumeDevices.contains(device)
-                        && mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
-                    scheduleMusicActiveCheck();
-                    int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device);
-                    if (index > safeMediaVolumeIndex(device)) {
-                        // Approximate cumulative active music time
-                        long curTimeMs = SystemClock.elapsedRealtime();
-                        if (mLastMusicActiveTimeMs != 0) {
-                            mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs);
-                        }
-                        mLastMusicActiveTimeMs = curTimeMs;
-                        Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs);
-                        if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
-                            setSafeMediaVolumeEnabled(true, caller);
-                            mMusicActiveMs = 0;
-                        }
-                        saveMusicActiveMs();
-                    }
-                } else {
-                    cancelMusicActiveCheck();
-                    mLastMusicActiveTimeMs = 0;
-                }
-            }
-        }
-    }
-
-    private void saveMusicActiveMs() {
-        mAudioHandler.obtainMessage(MSG_PERSIST_MUSIC_ACTIVE_MS, mMusicActiveMs, 0).sendToTarget();
-    }
-
-    private int getSafeUsbMediaVolumeIndex() {
-        // determine UI volume index corresponding to the wanted safe gain in dBFS
-        int min = MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
-        int max = MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
-
-        mSafeUsbMediaVolumeDbfs = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_safe_media_volume_usb_mB) / 100.0f;
-
-        while (Math.abs(max - min) > 1) {
-            int index = (max + min) / 2;
-            float gainDB = AudioSystem.getStreamVolumeDB(
-                    AudioSystem.STREAM_MUSIC, index, AudioSystem.DEVICE_OUT_USB_HEADSET);
-            if (Float.isNaN(gainDB)) {
-                //keep last min in case of read error
-                break;
-            } else if (gainDB == mSafeUsbMediaVolumeDbfs) {
-                min = index;
-                break;
-            } else if (gainDB < mSafeUsbMediaVolumeDbfs) {
-                min = index;
-            } else {
-                max = index;
-            }
-        }
-        return min * 10;
-    }
-
-    private void onConfigureSafeVolume(boolean force, String caller) {
-        synchronized (mSafeMediaVolumeStateLock) {
-            int mcc = mContext.getResources().getConfiguration().mcc;
-            if ((mMcc != mcc) || ((mMcc == 0) && force)) {
-                mSafeMediaVolumeIndex = mContext.getResources().getInteger(
-                        com.android.internal.R.integer.config_safe_media_volume_index) * 10;
-
-                mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
-
-                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);
-
-                // The persisted state is either "disabled" or "active": this is the state applied
-                // next time we boot and cannot be "inactive"
-                int persistedState;
-                if (safeMediaVolumeEnabled && !safeMediaVolumeBypass) {
-                    persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
-                    // The state can already be "inactive" here if the user has forced it before
-                    // the 30 seconds timeout for forced configuration. In this case we don't reset
-                    // it to "active".
-                    if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
-                        if (mMusicActiveMs == 0) {
-                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
-                            enforceSafeMediaVolume(caller);
-                        } else {
-                            // We have existing playback time recorded, already confirmed.
-                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
-                            mLastMusicActiveTimeMs = 0;
-                        }
-                    }
-                } else {
-                    persistedState = SAFE_MEDIA_VOLUME_DISABLED;
-                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
-                }
-                mMcc = mcc;
-                sendMsg(mAudioHandler,
-                        MSG_PERSIST_SAFE_VOLUME_STATE,
-                        SENDMSG_QUEUE,
-                        persistedState,
-                        0,
-                        null,
-                        0);
-            }
-        }
-    }
-
     ///////////////////////////////////////////////////////////////////////////
     // Internal methods
     ///////////////////////////////////////////////////////////////////////////
@@ -7711,7 +7521,7 @@
     //  2   mSetModeLock
     //  3     mSettingsLock
     //  4       VolumeStreamState.class
-    private class VolumeStreamState {
+    /*package*/ class VolumeStreamState {
         private final int mStreamType;
         private int mIndexMin;
         // min index when user doesn't have permission to change audio settings
@@ -8364,8 +8174,8 @@
         final VolumeStreamState streamState = mStreamStates[update.mStreamType];
         if (update.hasVolumeIndex()) {
             int index = update.getVolumeIndex();
-            if (!checkSafeMediaVolume(update.mStreamType, index, update.mDevice)) {
-                index = safeMediaVolumeIndex(update.mDevice);
+            if (!mSoundDoseHelper.checkSafeMediaVolume(update.mStreamType, index, update.mDevice)) {
+                index = mSoundDoseHelper.safeMediaVolumeIndex(update.mDevice);
             }
             streamState.setIndex(index, update.mDevice, update.mCaller,
                     // trusted as index is always validated before message is posted
@@ -8415,7 +8225,7 @@
     }
 
     /** Handles internal volume messages in separate volume thread. */
-    private class AudioHandler extends Handler {
+    /*package*/ class AudioHandler extends Handler {
 
         AudioHandler() {
             super();
@@ -8462,12 +8272,6 @@
             mSettings.putGlobalInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode);
         }
 
-        private void onPersistSafeVolumeState(int state) {
-            mSettings.putGlobalInt(mContentResolver,
-                    Settings.Global.AUDIO_SAFE_VOLUME_STATE,
-                    state);
-        }
-
         private void onNotifyVolumeEvent(@NonNull IAudioPolicyCallback apc,
                 @AudioManager.VolumeAdjustment int direction) {
             try {
@@ -8585,19 +8389,6 @@
                     mSpatializerHelper.reset(/* featureEnabled */ mHasSpatializerEffect);
                     break;
 
-                case MSG_CHECK_MUSIC_ACTIVE:
-                    onCheckMusicActive((String) msg.obj);
-                    break;
-
-                case MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED:
-                case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
-                    onConfigureSafeVolume((msg.what == MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED),
-                            (String) msg.obj);
-                    break;
-                case MSG_PERSIST_SAFE_VOLUME_STATE:
-                    onPersistSafeVolumeState(msg.arg1);
-                    break;
-
                 case MSG_SYSTEM_READY:
                     onSystemReady();
                     break;
@@ -8610,13 +8401,6 @@
                     onAccessoryPlugMediaUnmute(msg.arg1);
                     break;
 
-                case MSG_PERSIST_MUSIC_ACTIVE_MS:
-                    final int musicActiveMs = msg.arg1;
-                    mSettings.putSecureIntForUser(mContentResolver,
-                            Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
-                            UserHandle.USER_CURRENT);
-                    break;
-
                 case MSG_UNMUTE_STREAM:
                     onUnmuteStream(msg.arg1, msg.arg2);
                     break;
@@ -8746,6 +8530,12 @@
                 case MSG_NO_LOG_FOR_PLAYER_I:
                     mPlaybackMonitor.ignorePlayerIId(msg.arg1);
                     break;
+
+                default:
+                    if (msg.what >= SAFE_MEDIA_VOLUME_MSG_START) {
+                        // msg could be for the SoundDoseHelper
+                        mSoundDoseHelper.handleMessage(msg);
+                    }
             }
         }
     }
@@ -8862,8 +8652,8 @@
     /** only public for mocking/spying, do not call outside of AudioService */
     @VisibleForTesting
     public void checkMusicActive(int deviceType, String caller) {
-        if (mSafeMediaVolumeDevices.contains(deviceType)) {
-            scheduleMusicActiveCheck();
+        if (mSoundDoseHelper.safeDevicesContains(deviceType)) {
+            mSoundDoseHelper.scheduleMusicActiveCheck();
         }
     }
 
@@ -8997,7 +8787,8 @@
                     }
                 }
             } else if (action.equals(ACTION_CHECK_MUSIC_ACTIVE)) {
-                onCheckMusicActive(ACTION_CHECK_MUSIC_ACTIVE);
+                mSoundDoseHelper.onCheckMusicActive(ACTION_CHECK_MUSIC_ACTIVE,
+                        mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0));
             }
         }
     } // end class AudioServiceBroadcastReceiver
@@ -9883,13 +9674,7 @@
             // reading new configuration "safely" (i.e. under try catch) in case anything
             // goes wrong.
             Configuration config = context.getResources().getConfiguration();
-            sendMsg(mAudioHandler,
-                    MSG_CONFIGURE_SAFE_MEDIA_VOLUME,
-                    SENDMSG_REPLACE,
-                    0,
-                    0,
-                    TAG,
-                    0);
+            mSoundDoseHelper.configureSafeMediaVolume(/*forced*/false, TAG);
 
             boolean cameraSoundForced = readCameraSoundForced();
             synchronized (mSettingsLock) {
@@ -9947,143 +9732,10 @@
         return mDeviceBroker.startWatchingRoutes(observer);
     }
 
-
-    //==========================================================================================
-    // Safe media volume management.
-    // MUSIC stream volume level is limited when headphones are connected according to safety
-    // regulation. When the user attempts to raise the volume above the limit, a warning is
-    // displayed and the user has to acknowlegde before the volume is actually changed.
-    // The volume index corresponding to the limit is stored in config_safe_media_volume_index
-    // property. Platforms with a different limit must set this property accordingly in their
-    // overlay.
-    //==========================================================================================
-
-    // 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 int mSafeMediaVolumeState;
-    private final Object mSafeMediaVolumeStateLock = new Object();
-
-    private int mMcc = 0;
-    // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
-    private int mSafeMediaVolumeIndex;
-    // mSafeUsbMediaVolumeDbfs is the cached value of the config_safe_media_volume_usb_mB
-    // property, divided by 100.0.
-    private float mSafeUsbMediaVolumeDbfs;
-    // mSafeUsbMediaVolumeIndex is used for USB Headsets and is the music volume UI index
-    // corresponding to a gain of mSafeUsbMediaVolumeDbfs (defaulting to -37dB) in audio
-    // flinger mixer.
-    // We remove -22 dBs from the theoretical -15dB to account for the EQ + bass boost
-    // amplification when both effects are on with all band gains at maximum.
-    // This level corresponds to a loudness of 85 dB SPL for the warning to be displayed when
-    // the headset is compliant to EN 60950 with a max loudness of 100dB SPL.
-    private int mSafeUsbMediaVolumeIndex;
-    // mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
-    /*package*/ final Set<Integer> mSafeMediaVolumeDevices = new HashSet<>(
-            Arrays.asList(AudioSystem.DEVICE_OUT_WIRED_HEADSET,
-                    AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, AudioSystem.DEVICE_OUT_USB_HEADSET));
-    // 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 long mLastMusicActiveTimeMs = 0;
-    private PendingIntent mMusicActiveIntent = null;
-    private AlarmManager mAlarmManager;
-
-    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
-    private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;  // 30s after boot completed
-    // check playback or record activity every 6 seconds for UIDs owning mode IN_COMMUNICATION
-    private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
-
-    private static final String ACTION_CHECK_MUSIC_ACTIVE =
-            AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
-    private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
-
-    private int safeMediaVolumeIndex(int device) {
-        if (!mSafeMediaVolumeDevices.contains(device)) {
-            return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
-        }
-        if (device == AudioSystem.DEVICE_OUT_USB_HEADSET) {
-            return mSafeUsbMediaVolumeIndex;
-        } else {
-            return mSafeMediaVolumeIndex;
-        }
-    }
-
-    private void setSafeMediaVolumeEnabled(boolean on, String caller) {
-        synchronized (mSafeMediaVolumeStateLock) {
-            if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) &&
-                    (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_DISABLED)) {
-                if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) {
-                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
-                    enforceSafeMediaVolume(caller);
-                } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
-                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
-                    mMusicActiveMs = 1;  // nonzero = confirmed
-                    mLastMusicActiveTimeMs = 0;
-                    saveMusicActiveMs();
-                    scheduleMusicActiveCheck();
-                }
-            }
-        }
-    }
-
-    private void enforceSafeMediaVolume(String caller) {
-        VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
-        Set<Integer> devices = mSafeMediaVolumeDevices;
-
-        for (int device : devices) {
-            int index = streamState.getIndex(device);
-            if (index > safeMediaVolumeIndex(device)) {
-                streamState.setIndex(safeMediaVolumeIndex(device), device, caller,
-                            true /*hasModifyAudioSettings*/);
-                sendMsg(mAudioHandler,
-                        MSG_SET_DEVICE_VOLUME,
-                        SENDMSG_QUEUE,
-                        device,
-                        0,
-                        streamState,
-                        0);
-            }
-        }
-    }
-
-    private boolean checkSafeMediaVolume(int streamType, int index, int device) {
-        synchronized (mSafeMediaVolumeStateLock) {
-            if ((mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)
-                    && (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC)
-                    && (mSafeMediaVolumeDevices.contains(device))
-                    && (index > safeMediaVolumeIndex(device))) {
-                return false;
-            }
-            return true;
-        }
-    }
-
     @Override
     public void disableSafeMediaVolume(String callingPackage) {
         enforceVolumeController("disable the safe media volume");
-        synchronized (mSafeMediaVolumeStateLock) {
-            final long identity = Binder.clearCallingIdentity();
-            setSafeMediaVolumeEnabled(false, callingPackage);
-            Binder.restoreCallingIdentity(identity);
-            if (mPendingVolumeCommand != null) {
-                onSetStreamVolume(mPendingVolumeCommand.mStreamType,
-                                  mPendingVolumeCommand.mIndex,
-                                  mPendingVolumeCommand.mFlags,
-                                  mPendingVolumeCommand.mDevice,
-                                  callingPackage, true /*hasModifyAudioSettings*/);
-                mPendingVolumeCommand = null;
-            }
-        }
+        mSoundDoseHelper.disableSafeMediaVolume(callingPackage);
     }
 
     //==========================================================================================
@@ -10411,15 +10063,8 @@
 
         pw.println("\nOther state:");
         pw.print("  mVolumeController="); pw.println(mVolumeController);
-        pw.print("  mSafeMediaVolumeState=");
-        pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState));
-        pw.print("  mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex);
-        pw.print("  mSafeUsbMediaVolumeIndex="); pw.println(mSafeUsbMediaVolumeIndex);
-        pw.print("  mSafeUsbMediaVolumeDbfs="); pw.println(mSafeUsbMediaVolumeDbfs);
+        mSoundDoseHelper.dump(pw);
         pw.print("  sIndependentA11yVolume="); pw.println(sIndependentA11yVolume);
-        pw.print("  mPendingVolumeCommand="); pw.println(mPendingVolumeCommand);
-        pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
-        pw.print("  mMcc="); pw.println(mMcc);
         pw.print("  mCameraSoundForced="); pw.println(isCameraSoundForced());
         pw.print("  mHasVibrator="); pw.println(mHasVibrator);
         pw.print("  mVolumePolicy="); pw.println(mVolumePolicy);
@@ -10520,16 +10165,6 @@
     private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE
             + MediaMetrics.SEPARATOR;
 
-    private static String safeMediaVolumeStateToString(int state) {
-        switch(state) {
-            case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED";
-            case SAFE_MEDIA_VOLUME_DISABLED: return "SAFE_MEDIA_VOLUME_DISABLED";
-            case SAFE_MEDIA_VOLUME_INACTIVE: return "SAFE_MEDIA_VOLUME_INACTIVE";
-            case SAFE_MEDIA_VOLUME_ACTIVE: return "SAFE_MEDIA_VOLUME_ACTIVE";
-        }
-        return null;
-    }
-
     // Inform AudioFlinger of our device's low RAM attribute
     private static void readAndSetLowRamDevice()
     {
@@ -10609,7 +10244,14 @@
         }
     }
 
-    public class VolumeController {
+    /** Interface used for enforcing the safe hearing standard. */
+    public interface ISafeHearingVolumeController {
+        /** Displays an instructional safeguard as required by the safe hearing standard. */
+        void postDisplaySafeVolumeWarning(int flags);
+    }
+
+    /** Wrapper which encapsulates the {@link IVolumeController} functionality. */
+    public class VolumeController implements ISafeHearingVolumeController {
         private static final String TAG = "VolumeController";
 
         private IVolumeController mController;
@@ -10696,6 +10338,7 @@
             return "VolumeController(" + asBinder() + ",mVisible=" + mVisible + ")";
         }
 
+        @Override
         public void postDisplaySafeVolumeWarning(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
new file mode 100644
index 0000000..5188a53
--- /dev/null
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -0,0 +1,519 @@
+/*
+ * 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.server.audio;
+
+import static com.android.server.audio.AudioService.MAX_STREAM_VOLUME;
+import static com.android.server.audio.AudioService.MIN_STREAM_VOLUME;
+import static com.android.server.audio.AudioService.MSG_SET_DEVICE_VOLUME;
+import static com.android.server.audio.AudioService.SAFE_MEDIA_VOLUME_MSG_START;
+
+import android.annotation.NonNull;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioSystem;
+import android.os.Binder;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.MathUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.audio.AudioService.AudioHandler;
+import com.android.server.audio.AudioService.ISafeHearingVolumeController;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Safe media volume management.
+ * MUSIC stream volume level is limited when headphones are connected according to safety
+ * regulation. When the user attempts to raise the volume above the limit, a warning is
+ * displayed and the user has to acknowledge before the volume is actually changed.
+ * The volume index corresponding to the limit is stored in config_safe_media_volume_index
+ * property. Platforms with a different limit must set this property accordingly in their
+ * overlay.
+ */
+public class SoundDoseHelper {
+    private static final String TAG = "AS.SoundDoseHelper";
+
+    /*package*/ static final String ACTION_CHECK_MUSIC_ACTIVE =
+            AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+
+    /** Flag to enable/disable the sound dose computation. */
+    private static final boolean USE_CSD_FOR_SAFE_HEARING = false;
+
+    // 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 static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = SAFE_MEDIA_VOLUME_MSG_START + 1;
+    private static final int MSG_PERSIST_SAFE_VOLUME_STATE = SAFE_MEDIA_VOLUME_MSG_START + 2;
+    private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = SAFE_MEDIA_VOLUME_MSG_START + 3;
+
+    private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
+
+    // 30s after boot completed
+    private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;
+
+    private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
+    private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
+
+    private int mMcc = 0;
+
+    final Object mSafeMediaVolumeStateLock = new Object();
+    private int mSafeMediaVolumeState;
+
+    // Used when safe volume warning message display is requested by setStreamVolume(). In this
+    // case, the new requested volume, stream type and device are stored in mPendingVolumeCommand
+    // and used later when/if disableSafeMediaVolume() is called.
+    private StreamVolumeCommand mPendingVolumeCommand;
+
+    // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
+    private int mSafeMediaVolumeIndex;
+    // mSafeUsbMediaVolumeDbfs is the cached value of the config_safe_media_volume_usb_mB
+    // property, divided by 100.0.
+    private float mSafeUsbMediaVolumeDbfs;
+
+    // mSafeUsbMediaVolumeIndex is used for USB Headsets and is the music volume UI index
+    // corresponding to a gain of mSafeUsbMediaVolumeDbfs (defaulting to -37dB) in audio
+    // flinger mixer.
+    // We remove -22 dBs from the theoretical -15dB to account for the EQ + bass boost
+    // amplification when both effects are on with all band gains at maximum.
+    // This level corresponds to a loudness of 85 dB SPL for the warning to be displayed when
+    // the headset is compliant to EN 60950 with a max loudness of 100dB SPL.
+    private int mSafeUsbMediaVolumeIndex;
+    // mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
+    private final Set<Integer> mSafeMediaVolumeDevices = new HashSet<>(
+            Arrays.asList(AudioSystem.DEVICE_OUT_WIRED_HEADSET,
+                    AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, AudioSystem.DEVICE_OUT_USB_HEADSET));
+
+
+    // 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 long mLastMusicActiveTimeMs = 0;
+    private PendingIntent mMusicActiveIntent = null;
+    private final AlarmManager mAlarmManager;
+
+    @NonNull private final AudioService mAudioService;
+    @NonNull private final SettingsAdapter mSettings;
+    @NonNull private final AudioHandler mAudioHandler;
+    @NonNull private final ISafeHearingVolumeController mVolumeController;
+
+    private final Context mContext;
+
+    SoundDoseHelper(@NonNull AudioService audioService, Context context,
+            @NonNull AudioHandler audioHandler,
+            @NonNull SettingsAdapter settings,
+            @NonNull ISafeHearingVolumeController volumeController) {
+        mAudioService = audioService;
+        mAudioHandler = audioHandler;
+        mSettings = settings;
+        mVolumeController = volumeController;
+
+        mContext = context;
+
+        mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
+                Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
+        if (USE_CSD_FOR_SAFE_HEARING) {
+            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
+        }
+
+        // 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;
+
+        mAlarmManager = (AlarmManager) mContext.getSystemService(
+                Context.ALARM_SERVICE);
+    }
+
+    /*package*/ int safeMediaVolumeIndex(int device) {
+        if (!mSafeMediaVolumeDevices.contains(device)) {
+            return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+        }
+        if (device == AudioSystem.DEVICE_OUT_USB_HEADSET) {
+            return mSafeUsbMediaVolumeIndex;
+        } else {
+            return mSafeMediaVolumeIndex;
+        }
+    }
+
+    /*package*/ void restoreMusicActiveMs() {
+        synchronized (mSafeMediaVolumeStateLock) {
+            mMusicActiveMs = MathUtils.constrain(
+                    mSettings.getSecureIntForUser(mAudioService.getContentResolver(),
+                            Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, 0,
+                            UserHandle.USER_CURRENT),
+                    0, UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX);
+        }
+    }
+
+    /*package*/ void enforceSafeMediaVolumeIfActive(String caller) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) {
+                enforceSafeMediaVolume(caller);
+            }
+        }
+    }
+
+    /*package*/ void enforceSafeMediaVolume(String caller) {
+        AudioService.VolumeStreamState streamState = mAudioService.getVssVolumeForStream(
+                AudioSystem.STREAM_MUSIC);
+        Set<Integer> devices = mSafeMediaVolumeDevices;
+
+        for (int device : devices) {
+            int index = streamState.getIndex(device);
+            int safeIndex = safeMediaVolumeIndex(device);
+            if (index > safeIndex) {
+                streamState.setIndex(safeIndex, device, caller, true /*hasModifyAudioSettings*/);
+                mAudioHandler.sendMessageAtTime(
+                        mAudioHandler.obtainMessage(MSG_SET_DEVICE_VOLUME, device, /*arg2=*/0,
+                                streamState), /*delay=*/0);
+            }
+        }
+    }
+
+    /*package*/ boolean checkSafeMediaVolume(int streamType, int index, int device) {
+        boolean result;
+        synchronized (mSafeMediaVolumeStateLock) {
+            result = checkSafeMediaVolume_l(streamType, index, device);
+        }
+        return result;
+    }
+
+    @GuardedBy("mSafeMediaVolumeStateLock")
+    private boolean checkSafeMediaVolume_l(int streamType, int index, int device) {
+        return (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_ACTIVE)
+                    || (AudioService.mStreamVolumeAlias[streamType] != AudioSystem.STREAM_MUSIC)
+                    || (!mSafeMediaVolumeDevices.contains(device))
+                    || (index <= safeMediaVolumeIndex(device))
+                    || USE_CSD_FOR_SAFE_HEARING;
+    }
+
+    /*package*/ boolean willDisplayWarningAfterCheckVolume(int streamType, int index, int device,
+            int flags) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if (!checkSafeMediaVolume_l(streamType, index, device)) {
+                mVolumeController.postDisplaySafeVolumeWarning(flags);
+                mPendingVolumeCommand = new StreamVolumeCommand(
+                        streamType, index, flags, device);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /*package*/ void disableSafeMediaVolume(String callingPackage) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            final long identity = Binder.clearCallingIdentity();
+            setSafeMediaVolumeEnabled(false, callingPackage);
+            Binder.restoreCallingIdentity(identity);
+
+            if (mPendingVolumeCommand != null) {
+                mAudioService.onSetStreamVolume(mPendingVolumeCommand.mStreamType,
+                        mPendingVolumeCommand.mIndex,
+                        mPendingVolumeCommand.mFlags,
+                        mPendingVolumeCommand.mDevice,
+                        callingPackage, true /*hasModifyAudioSettings*/);
+                mPendingVolumeCommand = null;
+            }
+        }
+    }
+
+    /*package*/ void scheduleMusicActiveCheck() {
+        synchronized (mSafeMediaVolumeStateLock) {
+            cancelMusicActiveCheck();
+            if (!USE_CSD_FOR_SAFE_HEARING) {
+                mMusicActiveIntent = PendingIntent.getBroadcast(mContext,
+                        REQUEST_CODE_CHECK_MUSIC_ACTIVE,
+                        new Intent(ACTION_CHECK_MUSIC_ACTIVE),
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+                mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                        SystemClock.elapsedRealtime()
+                                + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent);
+            }
+        }
+    }
+
+    /*package*/ void onCheckMusicActive(String caller, boolean isStreamActive) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
+                int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC);
+                if (mSafeMediaVolumeDevices.contains(device) && isStreamActive) {
+                    scheduleMusicActiveCheck();
+                    int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC,
+                            device);
+                    if (index > safeMediaVolumeIndex(device)) {
+                        // Approximate cumulative active music time
+                        long curTimeMs = SystemClock.elapsedRealtime();
+                        if (mLastMusicActiveTimeMs != 0) {
+                            mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs);
+                        }
+                        mLastMusicActiveTimeMs = curTimeMs;
+                        Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs);
+                        if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
+                            setSafeMediaVolumeEnabled(true, caller);
+                            mMusicActiveMs = 0;
+                        }
+                        saveMusicActiveMs();
+                    }
+                } else {
+                    cancelMusicActiveCheck();
+                    mLastMusicActiveTimeMs = 0;
+                }
+            }
+        }
+    }
+
+    /*package*/ void configureSafeMediaVolume(boolean forced, String caller) {
+        int msg = MSG_CONFIGURE_SAFE_MEDIA_VOLUME;
+        mAudioHandler.removeMessages(msg);
+
+        long time = 0;
+        if (forced) {
+            time = (SystemClock.uptimeMillis() + (SystemProperties.getBoolean(
+                    "audio.safemedia.bypass", false) ? 0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS));
+        }
+        mAudioHandler.sendMessageAtTime(
+                mAudioHandler.obtainMessage(msg, /*arg1=*/forced ? 1 : 0, /*arg2=*/0, caller),
+                time);
+    }
+
+    /*package*/ void initSafeUsbMediaVolumeIndex() {
+        // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it
+        // relies on audio policy having correct ranges for volume indexes.
+        mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+    }
+
+    /*package*/ int getSafeMediaVolumeIndex(int device) {
+        if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE && mSafeMediaVolumeDevices.contains(
+                device)) {
+            return safeMediaVolumeIndex(device);
+        } else {
+            return -1;
+        }
+    }
+
+    /*package*/ boolean raiseVolumeDisplaySafeMediaVolume(int streamType, int index, int device,
+            int flags) {
+        if (checkSafeMediaVolume(streamType, index, device)) {
+            return false;
+        }
+
+        mVolumeController.postDisplaySafeVolumeWarning(flags);
+        return true;
+    }
+
+    /*package*/ boolean safeDevicesContains(int device) {
+        return mSafeMediaVolumeDevices.contains(device);
+    }
+
+    /*package*/ void invalidatPendingVolumeCommand() {
+        synchronized (mSafeMediaVolumeStateLock) {
+            mPendingVolumeCommand = null;
+        }
+    }
+
+    /*package*/ void handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
+                onConfigureSafeVolume((msg.arg1 == 1), (String) msg.obj);
+                break;
+            case MSG_PERSIST_SAFE_VOLUME_STATE:
+                onPersistSafeVolumeState(msg.arg1);
+                break;
+            case MSG_PERSIST_MUSIC_ACTIVE_MS:
+                final int musicActiveMs = msg.arg1;
+                mSettings.putSecureIntForUser(mAudioService.getContentResolver(),
+                        Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
+                        UserHandle.USER_CURRENT);
+                break;
+        }
+
+    }
+
+    /*package*/ void dump(PrintWriter pw) {
+        pw.print("  mSafeMediaVolumeState=");
+        pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState));
+        pw.print("  mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex);
+        pw.print("  mSafeUsbMediaVolumeIndex="); pw.println(mSafeUsbMediaVolumeIndex);
+        pw.print("  mSafeUsbMediaVolumeDbfs="); pw.println(mSafeUsbMediaVolumeDbfs);
+        pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
+        pw.print("  mMcc="); pw.println(mMcc);
+        pw.print("  mPendingVolumeCommand="); pw.println(mPendingVolumeCommand);
+    }
+
+    private void onConfigureSafeVolume(boolean force, String caller) {
+        synchronized (mSafeMediaVolumeStateLock) {
+            int mcc = mContext.getResources().getConfiguration().mcc;
+            if ((mMcc != mcc) || ((mMcc == 0) && force)) {
+                mSafeMediaVolumeIndex = mContext.getResources().getInteger(
+                        com.android.internal.R.integer.config_safe_media_volume_index) * 10;
+
+                mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+
+                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);
+
+                // The persisted state is either "disabled" or "active": this is the state applied
+                // next time we boot and cannot be "inactive"
+                int persistedState;
+                if (safeMediaVolumeEnabled && !safeMediaVolumeBypass && !USE_CSD_FOR_SAFE_HEARING) {
+                    persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
+                    // The state can already be "inactive" here if the user has forced it before
+                    // the 30 seconds timeout for forced configuration. In this case we don't reset
+                    // it to "active".
+                    if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
+                        if (mMusicActiveMs == 0) {
+                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
+                            enforceSafeMediaVolume(caller);
+                        } else {
+                            // We have existing playback time recorded, already confirmed.
+                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
+                            mLastMusicActiveTimeMs = 0;
+                        }
+                    }
+                } else {
+                    persistedState = SAFE_MEDIA_VOLUME_DISABLED;
+                    mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
+                }
+                mMcc = mcc;
+                mAudioHandler.sendMessageAtTime(
+                        mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE,
+                                persistedState, /*arg2=*/0,
+                                /*obj=*/null), /*delay=*/0);
+            }
+        }
+    }
+
+    @GuardedBy("mSafeMediaVolumeStateLock")
+    private void setSafeMediaVolumeEnabled(boolean on, String caller) {
+        if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) && (mSafeMediaVolumeState
+                != SAFE_MEDIA_VOLUME_DISABLED)) {
+            if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) {
+                mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
+                enforceSafeMediaVolume(caller);
+            } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
+                mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
+                mMusicActiveMs = 1;  // nonzero = confirmed
+                mLastMusicActiveTimeMs = 0;
+                saveMusicActiveMs();
+                scheduleMusicActiveCheck();
+            }
+        }
+    }
+
+    @GuardedBy("mSafeMediaVolumeStateLock")
+    private void cancelMusicActiveCheck() {
+        if (mMusicActiveIntent != null) {
+            mAlarmManager.cancel(mMusicActiveIntent);
+            mMusicActiveIntent = null;
+        }
+    }
+
+    @GuardedBy("mSafeMediaVolumeStateLock")
+    private void saveMusicActiveMs() {
+        mAudioHandler.obtainMessage(MSG_PERSIST_MUSIC_ACTIVE_MS, mMusicActiveMs, 0).sendToTarget();
+    }
+
+    private int getSafeUsbMediaVolumeIndex() {
+        // determine UI volume index corresponding to the wanted safe gain in dBFS
+        int min = MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+        int max = MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
+
+        mSafeUsbMediaVolumeDbfs = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_safe_media_volume_usb_mB) / 100.0f;
+
+        while (Math.abs(max - min) > 1) {
+            int index = (max + min) / 2;
+            float gainDB = AudioSystem.getStreamVolumeDB(
+                    AudioSystem.STREAM_MUSIC, index, AudioSystem.DEVICE_OUT_USB_HEADSET);
+            if (Float.isNaN(gainDB)) {
+                //keep last min in case of read error
+                break;
+            } else if (gainDB == mSafeUsbMediaVolumeDbfs) {
+                min = index;
+                break;
+            } else if (gainDB < mSafeUsbMediaVolumeDbfs) {
+                min = index;
+            } else {
+                max = index;
+            }
+        }
+        return min * 10;
+    }
+
+    private void onPersistSafeVolumeState(int state) {
+        mSettings.putGlobalInt(mAudioService.getContentResolver(),
+                Settings.Global.AUDIO_SAFE_VOLUME_STATE,
+                state);
+    }
+
+    private static String safeMediaVolumeStateToString(int state) {
+        switch(state) {
+            case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED";
+            case SAFE_MEDIA_VOLUME_DISABLED: return "SAFE_MEDIA_VOLUME_DISABLED";
+            case SAFE_MEDIA_VOLUME_INACTIVE: return "SAFE_MEDIA_VOLUME_INACTIVE";
+            case SAFE_MEDIA_VOLUME_ACTIVE: return "SAFE_MEDIA_VOLUME_ACTIVE";
+        }
+        return null;
+    }
+
+    // StreamVolumeCommand contains the information needed to defer the process of
+    // setStreamVolume() in case the user has to acknowledge the safe volume warning message.
+    private static class StreamVolumeCommand {
+        public final int mStreamType;
+        public final int mIndex;
+        public final int mFlags;
+        public final int mDevice;
+
+        StreamVolumeCommand(int streamType, int index, int flags, int device) {
+            mStreamType = streamType;
+            mIndex = index;
+            mFlags = flags;
+            mDevice = device;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=")
+                    .append(mIndex).append(",flags=").append(mFlags).append(",device=")
+                    .append(mDevice).append('}').toString();
+        }
+    }
+}