1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.audio;
18 
19 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
20 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
21 
22 import static com.android.server.audio.AudioService.MAX_STREAM_VOLUME;
23 import static com.android.server.audio.AudioService.MIN_STREAM_VOLUME;
24 import static com.android.server.audio.AudioService.MSG_SET_DEVICE_VOLUME;
25 import static com.android.server.audio.AudioService.SAFE_MEDIA_VOLUME_MSG_START;
26 
27 import static java.lang.Math.floor;
28 
29 import android.annotation.NonNull;
30 import android.app.AlarmManager;
31 import android.app.PendingIntent;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.media.AudioManager;
35 import android.media.AudioSystem;
36 import android.media.ISoundDose;
37 import android.media.ISoundDoseCallback;
38 import android.media.SoundDoseRecord;
39 import android.os.Binder;
40 import android.os.Message;
41 import android.os.RemoteException;
42 import android.os.SystemClock;
43 import android.os.SystemProperties;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.util.MathUtils;
49 import android.util.SparseIntArray;
50 
51 import com.android.internal.R;
52 import com.android.internal.annotations.GuardedBy;
53 import com.android.server.audio.AudioService.AudioHandler;
54 import com.android.server.audio.AudioService.ISafeHearingVolumeController;
55 import com.android.server.audio.AudioServiceEvents.SoundDoseEvent;
56 import com.android.server.utils.EventLogger;
57 
58 import java.io.PrintWriter;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.Collection;
62 import java.util.List;
63 import java.util.Objects;
64 import java.util.concurrent.atomic.AtomicBoolean;
65 import java.util.concurrent.atomic.AtomicReference;
66 import java.util.stream.Collectors;
67 
68 /**
69  * Safe media volume management.
70  * MUSIC stream volume level is limited when headphones are connected according to safety
71  * regulation. When the user attempts to raise the volume above the limit, a warning is
72  * displayed and the user has to acknowledge before the volume is actually changed.
73  * The volume index corresponding to the limit is stored in config_safe_media_volume_index
74  * property. Platforms with a different limit must set this property accordingly in their
75  * overlay.
76  */
77 public class SoundDoseHelper {
78     private static final String TAG = "AS.SoundDoseHelper";
79 
80     /*package*/ static final String ACTION_CHECK_MUSIC_ACTIVE =
81             "com.android.server.audio.action.CHECK_MUSIC_ACTIVE";
82 
83     /**
84      * Property to force the index based safe volume warnings. Note that usually when the
85      * CSD warnings are active the safe volume warnings are deactivated. In combination with
86      * {@link SoundDoseHelper#SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE} both approaches can be active
87      * at the same time.
88      */
89     private static final String SYSTEM_PROPERTY_SAFEMEDIA_FORCE = "audio.safemedia.force";
90     /** Property for bypassing the index based safe volume approach. */
91     private static final String SYSTEM_PROPERTY_SAFEMEDIA_BYPASS = "audio.safemedia.bypass";
92     /**
93      * Property to force the CSD warnings. Note that usually when the CSD warnings are active the
94      * safe volume warnings are deactivated. In combination with
95      * {@link SoundDoseHelper#SYSTEM_PROPERTY_SAFEMEDIA_FORCE} both approaches can be active
96      * at the same time.
97      */
98     private static final String SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE = "audio.safemedia.csd.force";
99 
100     // mSafeMediaVolumeState indicates whether the media volume is limited over headphones.
101     // It is SAFE_MEDIA_VOLUME_NOT_CONFIGURED at boot time until a network service is connected
102     // or the configure time is elapsed. It is then set to SAFE_MEDIA_VOLUME_ACTIVE or
103     // SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it
104     // can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume()
105     // (when user opts out).
106     private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0;
107     private static final int SAFE_MEDIA_VOLUME_DISABLED = 1;
108     private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2;  // confirmed
109     private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3;  // unconfirmed
110 
111     private static final int MSG_CONFIGURE_SAFE_MEDIA = SAFE_MEDIA_VOLUME_MSG_START + 1;
112     private static final int MSG_CONFIGURE_SAFE_MEDIA_FORCED = SAFE_MEDIA_VOLUME_MSG_START + 2;
113     private static final int MSG_PERSIST_SAFE_VOLUME_STATE = SAFE_MEDIA_VOLUME_MSG_START + 3;
114     private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = SAFE_MEDIA_VOLUME_MSG_START + 4;
115     private static final int MSG_PERSIST_CSD_VALUES = SAFE_MEDIA_VOLUME_MSG_START + 5;
116     /*package*/ static final int MSG_CSD_UPDATE_ATTENUATION = SAFE_MEDIA_VOLUME_MSG_START + 6;
117 
118     private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
119 
120     private static final int MOMENTARY_EXPOSURE_TIMEOUT_MS = (20 * 3600 * 1000); // 20 hours
121 
122     private static final int MOMENTARY_EXPOSURE_TIMEOUT_UNINITIALIZED = -1;
123 
124     // 30s after boot completed
125     private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;
126 
127     private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
128     private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
129 
130     // timeouts for the CSD warnings, -1 means no timeout (dialog must be ack'd by user)
131     private static final int CSD_WARNING_TIMEOUT_MS_DOSE_1X = 7000;
132     private static final int CSD_WARNING_TIMEOUT_MS_DOSE_5X = 5000;
133     private static final int CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START = -1;
134     private static final int CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE = 5000;
135 
136     private static final String PERSIST_CSD_RECORD_FIELD_SEPARATOR = ",";
137     private static final String PERSIST_CSD_RECORD_SEPARATOR_CHAR = "|";
138     private static final String PERSIST_CSD_RECORD_SEPARATOR = "\\|";
139 
140     private static final long GLOBAL_TIME_OFFSET_UNINITIALIZED = -1;
141 
142     private static final int SAFE_MEDIA_VOLUME_UNINITIALIZED = -1;
143 
144     private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
145             "CSD updates");
146 
147     private int mMcc = 0;
148 
149     private final Object mSafeMediaVolumeStateLock = new Object();
150     private int mSafeMediaVolumeState;
151 
152     // Used when safe volume warning message display is requested by setStreamVolume(). In this
153     // case, the new requested volume, stream type and device are stored in mPendingVolumeCommand
154     // and used later when/if disableSafeMediaVolume() is called.
155     private StreamVolumeCommand mPendingVolumeCommand;
156 
157     // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
158     private int mSafeMediaVolumeIndex;
159     // mSafeMediaVolumeDbfs is the cached value of the config_safe_media_volume_usb_mB
160     // property, divided by 100.0.
161     // For now using the same value for CSD supported devices
162     private float mSafeMediaVolumeDbfs;
163 
164     /**
165      * mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced.
166      * Contains a safe volume index for a given device type.
167      * Indexes are used for headsets and is the music volume UI index
168      * corresponding to a gain of mSafeMediaVolumeDbfs (defaulting to -37dB) in audio
169      * flinger mixer.
170      * We remove -22 dBs from the theoretical -15dB to account for the EQ + bass boost
171      * amplification when both effects are on with all band gains at maximum.
172      * This level corresponds to a loudness of 85 dB SPL for the warning to be displayed when
173      * the headset is compliant to EN 60950 with a max loudness of 100dB SPL.
174      */
175     private final SparseIntArray mSafeMediaVolumeDevices = new SparseIntArray();
176 
177     // mMusicActiveMs is the cumulative time of music activity since safe volume was disabled.
178     // When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
179     // automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
180     private int mMusicActiveMs;
181     private long mLastMusicActiveTimeMs = 0;
182     private PendingIntent mMusicActiveIntent = null;
183     private final AlarmManager mAlarmManager;
184 
185     @NonNull private final AudioService mAudioService;
186     @NonNull private final SettingsAdapter mSettings;
187     @NonNull private final AudioHandler mAudioHandler;
188     @NonNull private final ISafeHearingVolumeController mVolumeController;
189 
190     private final AtomicBoolean mEnableCsd = new AtomicBoolean(false);
191 
192     private final Object mCsdAsAFeatureLock = new Object();
193 
194     @GuardedBy("mCsdAsAFeatureLock")
195     private boolean mIsCsdAsAFeatureAvailable = false;
196 
197     @GuardedBy("mCsdAsAFeatureLock")
198     private boolean mIsCsdAsAFeatureEnabled = false;
199 
200     private final ArrayList<ISoundDose.AudioDeviceCategory> mCachedAudioDeviceCategories =
201             new ArrayList<>();
202 
203     private final Object mCsdStateLock = new Object();
204 
205     private final AtomicReference<ISoundDose> mSoundDose = new AtomicReference<>();
206 
207     @GuardedBy("mCsdStateLock")
208     private float mCurrentCsd = 0.f;
209 
210     @GuardedBy("mCsdStateLock")
211     private long mLastMomentaryExposureTimeMs = MOMENTARY_EXPOSURE_TIMEOUT_UNINITIALIZED;
212 
213     // dose at which the next dose reached warning occurs
214     @GuardedBy("mCsdStateLock")
215     private float mNextCsdWarning = 1.0f;
216     @GuardedBy("mCsdStateLock")
217     private final List<SoundDoseRecord> mDoseRecords = new ArrayList<>();
218 
219     // time in seconds reported by System.currentTimeInMillis used as an offset to convert between
220     // boot time and global time
221     @GuardedBy("mCsdStateLock")
222     private long mGlobalTimeOffsetInSecs = GLOBAL_TIME_OFFSET_UNINITIALIZED;
223 
224     private final Context mContext;
225 
226     private final ISoundDoseCallback.Stub mSoundDoseCallback = new ISoundDoseCallback.Stub() {
227         public void onMomentaryExposure(float currentMel, int deviceId) {
228             if (!mEnableCsd.get()) {
229                 Log.w(TAG, "onMomentaryExposure: csd not supported, ignoring callback");
230                 return;
231             }
232 
233             Log.w(TAG, "DeviceId " + deviceId + " triggered momentary exposure with value: "
234                     + currentMel);
235             mLogger.enqueue(SoundDoseEvent.getMomentaryExposureEvent(currentMel));
236 
237             boolean postWarning = false;
238             synchronized (mCsdStateLock) {
239                 if (mLastMomentaryExposureTimeMs < 0
240                         || (System.currentTimeMillis() - mLastMomentaryExposureTimeMs)
241                         >= MOMENTARY_EXPOSURE_TIMEOUT_MS) {
242                     mLastMomentaryExposureTimeMs = System.currentTimeMillis();
243                     postWarning = true;
244                 }
245             }
246 
247             if (postWarning) {
248                 mVolumeController.postDisplayCsdWarning(
249                         AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE,
250                         getTimeoutMsForWarning(AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE));
251             }
252         }
253 
254         public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) {
255             if (!mEnableCsd.get()) {
256                 Log.w(TAG, "onNewCsdValue: csd not supported, ignoring value");
257                 return;
258             }
259 
260             Log.i(TAG, "onNewCsdValue: " + currentCsd);
261             synchronized (mCsdStateLock) {
262                 if (mCurrentCsd < currentCsd) {
263                     // dose increase: going over next threshold?
264                     if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
265                         if (mNextCsdWarning == 5.0f) {
266                             // 500% dose repeat
267                             mVolumeController.postDisplayCsdWarning(
268                                     AudioManager.CSD_WARNING_DOSE_REPEATED_5X,
269                                     getTimeoutMsForWarning(
270                                             AudioManager.CSD_WARNING_DOSE_REPEATED_5X));
271                             // on the 5x dose warning, the volume reduction happens right away
272                             mAudioService.postLowerVolumeToRs1();
273                         } else {
274                             mVolumeController.postDisplayCsdWarning(
275                                     AudioManager.CSD_WARNING_DOSE_REACHED_1X,
276                                     getTimeoutMsForWarning(
277                                             AudioManager.CSD_WARNING_DOSE_REACHED_1X));
278                         }
279                         mNextCsdWarning += 1.0f;
280                     }
281                 } else {
282                     // dose decrease: dropping below previous threshold of warning?
283                     if ((currentCsd < mNextCsdWarning - 1.0f) && (
284                             mNextCsdWarning >= 2.0f)) {
285                         mNextCsdWarning -= 1.0f;
286                     }
287                 }
288                 mCurrentCsd = currentCsd;
289                 updateSoundDoseRecords_l(records, currentCsd);
290             }
291         }
292     };
293 
SoundDoseHelper(@onNull AudioService audioService, Context context, @NonNull AudioHandler audioHandler, @NonNull SettingsAdapter settings, @NonNull ISafeHearingVolumeController volumeController)294     SoundDoseHelper(@NonNull AudioService audioService, Context context,
295             @NonNull AudioHandler audioHandler,
296             @NonNull SettingsAdapter settings,
297             @NonNull ISafeHearingVolumeController volumeController) {
298         mAudioService = audioService;
299         mAudioHandler = audioHandler;
300         mSettings = settings;
301         mVolumeController = volumeController;
302 
303         mContext = context;
304 
305         initSafeVolumes();
306 
307         mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
308                 Settings.Global.AUDIO_SAFE_VOLUME_STATE, SAFE_MEDIA_VOLUME_NOT_CONFIGURED);
309 
310         // The default safe volume index read here will be replaced by the actual value when
311         // the mcc is read by onConfigureSafeMedia()
312         // For now we use the same index for RS2 initial warning with CSD
313         mSafeMediaVolumeIndex = mContext.getResources().getInteger(
314                 R.integer.config_safe_media_volume_index) * 10;
315 
316         mSoundDose.set(AudioSystem.getSoundDoseInterface(mSoundDoseCallback));
317         // Csd will be initially disabled until the mcc is read in onConfigureSafeMedia()
318         initCsd();
319 
320         mAlarmManager = (AlarmManager) mContext.getSystemService(
321                 Context.ALARM_SERVICE);
322     }
323 
initSafeVolumes()324     void initSafeVolumes() {
325         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_WIRED_HEADSET,
326                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
327         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE,
328                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
329         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_USB_HEADSET,
330                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
331         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_BLE_HEADSET,
332                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
333         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_BLE_BROADCAST,
334                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
335         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES,
336                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
337         mSafeMediaVolumeDevices.append(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
338                 SAFE_MEDIA_VOLUME_UNINITIALIZED);
339     }
340 
getOutputRs2UpperBound()341     float getOutputRs2UpperBound() {
342         if (!mEnableCsd.get()) {
343             return 0.f;
344         }
345 
346         final ISoundDose soundDose = mSoundDose.get();
347         if (soundDose == null) {
348             Log.w(TAG, "Sound dose interface not initialized");
349             return 0.f;
350         }
351 
352         try {
353             return soundDose.getOutputRs2UpperBound();
354         } catch (RemoteException e) {
355             Log.e(TAG, "Exception while getting the RS2 exposure value", e);
356             return 0.f;
357         }
358     }
359 
setOutputRs2UpperBound(float rs2Value)360     void setOutputRs2UpperBound(float rs2Value) {
361         if (!mEnableCsd.get()) {
362             return;
363         }
364 
365         final ISoundDose soundDose = mSoundDose.get();
366         if (soundDose == null) {
367             Log.w(TAG, "Sound dose interface not initialized");
368             return;
369         }
370 
371         try {
372             soundDose.setOutputRs2UpperBound(rs2Value);
373         } catch (RemoteException e) {
374             Log.e(TAG, "Exception while setting the RS2 exposure value", e);
375         }
376     }
377 
getCsd()378     float getCsd() {
379         if (!mEnableCsd.get()) {
380             return -1.f;
381         }
382 
383         final ISoundDose soundDose = mSoundDose.get();
384         if (soundDose == null) {
385             Log.w(TAG, "Sound dose interface not initialized");
386             return -1.f;
387         }
388 
389         try {
390             return soundDose.getCsd();
391         } catch (RemoteException e) {
392             Log.e(TAG, "Exception while getting the CSD value", e);
393             return -1.f;
394         }
395     }
396 
setCsd(float csd)397     void setCsd(float csd) {
398         if (!mEnableCsd.get()) {
399             return;
400         }
401 
402         SoundDoseRecord[] doseRecordsArray;
403         synchronized (mCsdStateLock) {
404             mCurrentCsd = csd;
405             mNextCsdWarning = (float) floor(csd + 1.0);
406 
407             mDoseRecords.clear();
408 
409             if (mCurrentCsd > 0.0f) {
410                 final SoundDoseRecord record = new SoundDoseRecord();
411                 record.timestamp = SystemClock.elapsedRealtime() / 1000;
412                 record.value = csd;
413                 mDoseRecords.add(record);
414             }
415             doseRecordsArray = mDoseRecords.toArray(new SoundDoseRecord[0]);
416         }
417 
418         final ISoundDose soundDose = mSoundDose.get();
419         if (soundDose == null) {
420             Log.w(TAG, "Sound dose interface not initialized");
421             return;
422         }
423 
424         try {
425             soundDose.resetCsd(csd, doseRecordsArray);
426         } catch (RemoteException e) {
427             Log.e(TAG, "Exception while setting the CSD value", e);
428         }
429     }
430 
resetCsdTimeouts()431     void resetCsdTimeouts() {
432         if (!mEnableCsd.get()) {
433             return;
434         }
435 
436         synchronized (mCsdStateLock) {
437             mLastMomentaryExposureTimeMs = MOMENTARY_EXPOSURE_TIMEOUT_UNINITIALIZED;
438         }
439     }
440 
forceUseFrameworkMel(boolean useFrameworkMel)441     void forceUseFrameworkMel(boolean useFrameworkMel) {
442         if (!mEnableCsd.get()) {
443             return;
444         }
445 
446         final ISoundDose soundDose = mSoundDose.get();
447         if (soundDose == null) {
448             Log.w(TAG, "Sound dose interface not initialized");
449             return;
450         }
451 
452         try {
453             soundDose.forceUseFrameworkMel(useFrameworkMel);
454         } catch (RemoteException e) {
455             Log.e(TAG, "Exception while forcing the internal MEL computation", e);
456         }
457     }
458 
forceComputeCsdOnAllDevices(boolean computeCsdOnAllDevices)459     void forceComputeCsdOnAllDevices(boolean computeCsdOnAllDevices) {
460         if (!mEnableCsd.get()) {
461             return;
462         }
463 
464         final ISoundDose soundDose = mSoundDose.get();
465         if (soundDose == null) {
466             Log.w(TAG, "Sound dose interface not initialized");
467             return;
468         }
469 
470         try {
471             soundDose.forceComputeCsdOnAllDevices(computeCsdOnAllDevices);
472         } catch (RemoteException e) {
473             Log.e(TAG, "Exception while forcing CSD computation on all devices", e);
474         }
475     }
476 
isCsdEnabled()477     boolean isCsdEnabled() {
478         if (!mEnableCsd.get()) {
479             return false;
480         }
481 
482         final ISoundDose soundDose = mSoundDose.get();
483         if (soundDose == null) {
484             Log.w(TAG, "Sound dose interface not initialized");
485             return false;
486         }
487 
488         try {
489             return soundDose.isSoundDoseHalSupported();
490         } catch (RemoteException e) {
491             Log.e(TAG, "Exception while forcing CSD computation on all devices", e);
492         }
493         return false;
494     }
495 
isCsdAsAFeatureAvailable()496     boolean isCsdAsAFeatureAvailable() {
497         synchronized (mCsdAsAFeatureLock) {
498             return mIsCsdAsAFeatureAvailable;
499         }
500     }
501 
isCsdAsAFeatureEnabled()502     boolean isCsdAsAFeatureEnabled() {
503         synchronized (mCsdAsAFeatureLock) {
504             return mIsCsdAsAFeatureEnabled;
505         }
506     }
507 
setCsdAsAFeatureEnabled(boolean csdAsAFeatureEnabled)508     void setCsdAsAFeatureEnabled(boolean csdAsAFeatureEnabled) {
509         boolean doUpdate;
510         synchronized (mCsdAsAFeatureLock) {
511             doUpdate = mIsCsdAsAFeatureEnabled != csdAsAFeatureEnabled && mIsCsdAsAFeatureAvailable;
512             mIsCsdAsAFeatureEnabled = csdAsAFeatureEnabled;
513             final long callingIdentity = Binder.clearCallingIdentity();
514             try {
515                 mSettings.putSecureIntForUser(mAudioService.getContentResolver(),
516                         Settings.Secure.AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED,
517                         mIsCsdAsAFeatureEnabled ? 1 : 0, UserHandle.USER_CURRENT);
518             } finally {
519                 Binder.restoreCallingIdentity(callingIdentity);
520             }
521         }
522 
523         if (doUpdate) {
524             updateCsdEnabled("setCsdAsAFeatureEnabled");
525         }
526     }
527 
setAudioDeviceCategory(String address, int internalAudioType, boolean isHeadphone)528     void setAudioDeviceCategory(String address, int internalAudioType, boolean isHeadphone) {
529         if (!mEnableCsd.get()) {
530             return;
531         }
532 
533         final ISoundDose soundDose = mSoundDose.get();
534         if (soundDose == null) {
535             Log.w(TAG, "Sound dose interface not initialized");
536             return;
537         }
538 
539         try {
540             final ISoundDose.AudioDeviceCategory audioDeviceCategory =
541                     new ISoundDose.AudioDeviceCategory();
542             audioDeviceCategory.address = address;
543             audioDeviceCategory.internalAudioType = internalAudioType;
544             audioDeviceCategory.csdCompatible = isHeadphone;
545             soundDose.setAudioDeviceCategory(audioDeviceCategory);
546         } catch (RemoteException e) {
547             Log.e(TAG, "Exception while forcing the internal MEL computation", e);
548         }
549     }
550 
initCachedAudioDeviceCategories(Collection<AdiDeviceState> deviceStates)551     void initCachedAudioDeviceCategories(Collection<AdiDeviceState> deviceStates) {
552         for (final AdiDeviceState state : deviceStates) {
553             if (state.getAudioDeviceCategory() != AUDIO_DEVICE_CATEGORY_UNKNOWN) {
554                 final ISoundDose.AudioDeviceCategory audioDeviceCategory =
555                         new ISoundDose.AudioDeviceCategory();
556                 audioDeviceCategory.address = state.getDeviceAddress();
557                 audioDeviceCategory.internalAudioType = state.getInternalDeviceType();
558                 audioDeviceCategory.csdCompatible =
559                         state.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_HEADPHONES;
560                 mCachedAudioDeviceCategories.add(audioDeviceCategory);
561             }
562         }
563     }
564 
safeMediaVolumeIndex(int device)565     /*package*/ int safeMediaVolumeIndex(int device) {
566         final int vol = mSafeMediaVolumeDevices.get(device);
567         if (vol == SAFE_MEDIA_VOLUME_UNINITIALIZED) {
568             return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
569         }
570 
571         return vol;
572     }
573 
restoreMusicActiveMs()574     /*package*/ void restoreMusicActiveMs() {
575         synchronized (mSafeMediaVolumeStateLock) {
576             mMusicActiveMs = MathUtils.constrain(
577                     mSettings.getSecureIntForUser(mAudioService.getContentResolver(),
578                             Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, 0,
579                             UserHandle.USER_CURRENT),
580                     0, UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX);
581         }
582     }
583 
enforceSafeMediaVolumeIfActive(String caller)584     /*package*/ void enforceSafeMediaVolumeIfActive(String caller) {
585         synchronized (mSafeMediaVolumeStateLock) {
586             if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) {
587                 enforceSafeMediaVolume(caller);
588             }
589         }
590     }
591 
enforceSafeMediaVolume(String caller)592     /*package*/ void enforceSafeMediaVolume(String caller) {
593         AudioService.VolumeStreamState streamState = mAudioService.getVssVolumeForStream(
594                 AudioSystem.STREAM_MUSIC);
595 
596         for (int i = 0; i < mSafeMediaVolumeDevices.size(); ++i)  {
597             int deviceType = mSafeMediaVolumeDevices.keyAt(i);
598             int index = streamState.getIndex(deviceType);
599             int safeIndex = safeMediaVolumeIndex(deviceType);
600             if (index > safeIndex) {
601                 streamState.setIndex(safeIndex, deviceType, caller,
602                         true /*hasModifyAudioSettings*/);
603                 mAudioHandler.sendMessageAtTime(
604                         mAudioHandler.obtainMessage(MSG_SET_DEVICE_VOLUME, deviceType,
605                                 /*arg2=*/0, streamState), /*delay=*/0);
606             }
607         }
608     }
609 
610     /**
611      * Returns {@code true} if the safe media actions can be applied for the given stream type,
612      * volume index and device.
613      **/
checkSafeMediaVolume(int streamType, int index, int device)614     /*package*/ boolean checkSafeMediaVolume(int streamType, int index, int device) {
615         boolean result;
616         synchronized (mSafeMediaVolumeStateLock) {
617             result = checkSafeMediaVolume_l(streamType, index, device);
618         }
619         return result;
620     }
621 
622     @GuardedBy("mSafeMediaVolumeStateLock")
checkSafeMediaVolume_l(int streamType, int index, int device)623     private boolean checkSafeMediaVolume_l(int streamType, int index, int device) {
624         return (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)
625                     && (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC)
626                     && safeDevicesContains(device)
627                     && (index > safeMediaVolumeIndex(device));
628     }
629 
willDisplayWarningAfterCheckVolume(int streamType, int index, int device, int flags)630     /*package*/ boolean willDisplayWarningAfterCheckVolume(int streamType, int index, int device,
631             int flags) {
632         synchronized (mSafeMediaVolumeStateLock) {
633             if (checkSafeMediaVolume_l(streamType, index, device)) {
634                 mVolumeController.postDisplaySafeVolumeWarning(flags);
635                 mPendingVolumeCommand = new StreamVolumeCommand(
636                         streamType, index, flags, device);
637                 return true;
638             }
639         }
640         return false;
641     }
642 
disableSafeMediaVolume(String callingPackage)643     /*package*/ void disableSafeMediaVolume(String callingPackage) {
644         synchronized (mSafeMediaVolumeStateLock) {
645             final long identity = Binder.clearCallingIdentity();
646             setSafeMediaVolumeEnabled(false, callingPackage);
647             Binder.restoreCallingIdentity(identity);
648 
649             if (mPendingVolumeCommand != null) {
650                 mAudioService.onSetStreamVolume(mPendingVolumeCommand.mStreamType,
651                         mPendingVolumeCommand.mIndex,
652                         mPendingVolumeCommand.mFlags,
653                         mPendingVolumeCommand.mDevice,
654                         callingPackage, true /*hasModifyAudioSettings*/,
655                         true /*canChangeMute*/);
656                 mPendingVolumeCommand = null;
657             }
658         }
659     }
660 
scheduleMusicActiveCheck()661     /*package*/ void scheduleMusicActiveCheck() {
662         synchronized (mSafeMediaVolumeStateLock) {
663             cancelMusicActiveCheck();
664             mMusicActiveIntent = PendingIntent.getBroadcast(mContext,
665                     REQUEST_CODE_CHECK_MUSIC_ACTIVE,
666                     new Intent(ACTION_CHECK_MUSIC_ACTIVE),
667                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
668             mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
669                     SystemClock.elapsedRealtime()
670                             + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent);
671         }
672     }
673 
onCheckMusicActive(String caller, boolean isStreamActive)674     /*package*/ void onCheckMusicActive(String caller, boolean isStreamActive) {
675         synchronized (mSafeMediaVolumeStateLock) {
676             if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
677                 int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC);
678                 if (safeDevicesContains(device) && isStreamActive) {
679                     scheduleMusicActiveCheck();
680                     int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC,
681                             device);
682                     if (index > safeMediaVolumeIndex(device)) {
683                         // Approximate cumulative active music time
684                         long curTimeMs = SystemClock.elapsedRealtime();
685                         if (mLastMusicActiveTimeMs != 0) {
686                             mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs);
687                         }
688                         mLastMusicActiveTimeMs = curTimeMs;
689                         Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs);
690                         if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
691                             setSafeMediaVolumeEnabled(true, caller);
692                             mMusicActiveMs = 0;
693                         }
694                         saveMusicActiveMs();
695                     }
696                 } else {
697                     cancelMusicActiveCheck();
698                     mLastMusicActiveTimeMs = 0;
699                 }
700             }
701         }
702     }
703 
configureSafeMedia(boolean forced, String caller)704     /*package*/ void configureSafeMedia(boolean forced, String caller) {
705         int msg = forced ? MSG_CONFIGURE_SAFE_MEDIA_FORCED : MSG_CONFIGURE_SAFE_MEDIA;
706         mAudioHandler.removeMessages(msg);
707 
708         long time = 0;
709         if (forced) {
710             time = (SystemClock.uptimeMillis() + (SystemProperties.getBoolean(
711                     "audio.safemedia.bypass", false) ? 0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS));
712         }
713 
714         mAudioHandler.sendMessageAtTime(
715                 mAudioHandler.obtainMessage(msg, /*arg1=*/0, /*arg2=*/0, caller),
716                 time);
717     }
718 
initSafeMediaVolumeIndex()719     /*package*/ void initSafeMediaVolumeIndex() {
720         for (int i = 0; i < mSafeMediaVolumeDevices.size(); ++i)  {
721             int deviceType = mSafeMediaVolumeDevices.keyAt(i);
722             if (mSafeMediaVolumeDevices.valueAt(i) == SAFE_MEDIA_VOLUME_UNINITIALIZED) {
723                 mSafeMediaVolumeDevices.put(deviceType, getSafeDeviceMediaVolumeIndex(deviceType));
724             }
725         }
726     }
727 
getSafeMediaVolumeIndex(int device)728     /*package*/ int getSafeMediaVolumeIndex(int device) {
729         if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE
730                 && safeDevicesContains(device)) {
731             return safeMediaVolumeIndex(device);
732         } else {
733             return -1;
734         }
735     }
736 
raiseVolumeDisplaySafeMediaVolume(int streamType, int index, int device, int flags)737     /*package*/ boolean raiseVolumeDisplaySafeMediaVolume(int streamType, int index, int device,
738             int flags) {
739         if (!checkSafeMediaVolume(streamType, index, device)) {
740             return false;
741         }
742 
743         mVolumeController.postDisplaySafeVolumeWarning(flags);
744         return true;
745     }
746 
safeDevicesContains(int device)747     /*package*/ boolean safeDevicesContains(int device) {
748         return mSafeMediaVolumeDevices.get(device, SAFE_MEDIA_VOLUME_UNINITIALIZED) >= 0;
749     }
750 
invalidatPendingVolumeCommand()751     /*package*/ void invalidatPendingVolumeCommand() {
752         synchronized (mSafeMediaVolumeStateLock) {
753             mPendingVolumeCommand = null;
754         }
755     }
756 
handleMessage(Message msg)757     /*package*/ void handleMessage(Message msg) {
758         switch (msg.what) {
759             case MSG_CONFIGURE_SAFE_MEDIA_FORCED:
760             case MSG_CONFIGURE_SAFE_MEDIA:
761                 onConfigureSafeMedia((msg.what == MSG_CONFIGURE_SAFE_MEDIA_FORCED),
762                         (String) msg.obj);
763                 break;
764             case MSG_PERSIST_SAFE_VOLUME_STATE:
765                 onPersistSafeVolumeState(msg.arg1);
766                 break;
767             case MSG_PERSIST_MUSIC_ACTIVE_MS:
768                 final int musicActiveMs = msg.arg1;
769                 mSettings.putSecureIntForUser(mAudioService.getContentResolver(),
770                         Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
771                         UserHandle.USER_CURRENT);
772                 break;
773             case MSG_PERSIST_CSD_VALUES:
774                 onPersistSoundDoseRecords();
775                 break;
776             case MSG_CSD_UPDATE_ATTENUATION:
777                 final int device = msg.arg1;
778                 final boolean isAbsoluteVolume = (msg.arg2 == 1);
779                 final AudioService.VolumeStreamState streamState =
780                         (AudioService.VolumeStreamState) msg.obj;
781 
782                 updateDoseAttenuation(streamState.getIndex(device), device,
783                         streamState.getStreamType(), isAbsoluteVolume);
784                 break;
785             default:
786                 Log.e(TAG, "Unexpected msg to handle: " + msg.what);
787                 break;
788         }
789     }
790 
dump(PrintWriter pw)791     /*package*/ void dump(PrintWriter pw) {
792         pw.print("  mEnableCsd="); pw.println(mEnableCsd.get());
793         if (mEnableCsd.get()) {
794             synchronized (mCsdStateLock) {
795                 pw.print("  mCurrentCsd="); pw.println(mCurrentCsd);
796             }
797         }
798         pw.print("  mSafeMediaVolumeState=");
799         pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState));
800         pw.print("  mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex);
801         for (int i = 0; i < mSafeMediaVolumeDevices.size(); ++i)  {
802             pw.print("  mSafeMediaVolumeIndex["); pw.print(mSafeMediaVolumeDevices.keyAt(i));
803             pw.print("]="); pw.println(mSafeMediaVolumeDevices.valueAt(i));
804         }
805         pw.print("  mSafeMediaVolumeDbfs="); pw.println(mSafeMediaVolumeDbfs);
806         pw.print("  mMusicActiveMs="); pw.println(mMusicActiveMs);
807         pw.print("  mMcc="); pw.println(mMcc);
808         pw.print("  mPendingVolumeCommand="); pw.println(mPendingVolumeCommand);
809         pw.println();
810         mLogger.dump(pw);
811         pw.println();
812     }
813 
reset()814     /*package*/void  reset() {
815         Log.d(TAG, "Reset the sound dose helper");
816 
817         mSoundDose.compareAndExchange(/*expectedValue=*/null,
818                 AudioSystem.getSoundDoseInterface(mSoundDoseCallback));
819 
820         synchronized (mCsdStateLock) {
821             try {
822                 final ISoundDose soundDose = mSoundDose.get();
823                 if (soundDose != null && soundDose.asBinder().isBinderAlive()) {
824                     if (mCurrentCsd != 0.f) {
825                         Log.d(TAG,
826                                 "Resetting the saved sound dose value " + mCurrentCsd);
827                         SoundDoseRecord[] records = mDoseRecords.toArray(
828                                 new SoundDoseRecord[0]);
829                         soundDose.resetCsd(mCurrentCsd, records);
830                     }
831                 }
832             } catch (RemoteException e) {
833                 // noop
834             }
835         }
836     }
837 
updateDoseAttenuation(int newIndex, int device, int streamType, boolean isAbsoluteVolume)838     private void updateDoseAttenuation(int newIndex, int device, int streamType,
839             boolean isAbsoluteVolume) {
840         if (!mEnableCsd.get()) {
841             return;
842         }
843 
844         final ISoundDose soundDose = mSoundDose.get();
845         if (soundDose == null) {
846             Log.w(TAG,  "Can not apply attenuation. ISoundDose itf is null.");
847             return;
848         }
849 
850         try {
851             if (!isAbsoluteVolume) {
852                 // remove any possible previous attenuation
853                 soundDose.updateAttenuation(/* attenuationDB= */0.f, device);
854 
855                 return;
856             }
857 
858             if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC
859                     && safeDevicesContains(device)) {
860                 soundDose.updateAttenuation(
861                         AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
862                                 (newIndex + 5) / 10,
863                                 device), device);
864             }
865         } catch (RemoteException e) {
866             Log.e(TAG, "Could not apply the attenuation for MEL calculation with volume index "
867                     + newIndex, e);
868         }
869     }
870 
initCsd()871     private void initCsd() {
872         ISoundDose soundDose = mSoundDose.get();
873         if (soundDose == null) {
874             Log.w(TAG, "ISoundDose instance is null.");
875             return;
876         }
877 
878         try {
879             soundDose.setCsdEnabled(mEnableCsd.get());
880         } catch (RemoteException e) {
881             Log.e(TAG, "Cannot disable CSD", e);
882         }
883 
884         if (!mEnableCsd.get()) {
885             return;
886         }
887 
888         Log.v(TAG, "Initializing sound dose");
889 
890         try {
891             if (mCachedAudioDeviceCategories.size() > 0) {
892                 soundDose.initCachedAudioDeviceCategories(mCachedAudioDeviceCategories.toArray(
893                         new ISoundDose.AudioDeviceCategory[0]));
894                 mCachedAudioDeviceCategories.clear();
895             }
896         } catch (RemoteException e) {
897             Log.e(TAG, "Exception while forcing the internal MEL computation", e);
898         }
899 
900         synchronized (mCsdAsAFeatureLock) {
901             mIsCsdAsAFeatureEnabled = mSettings.getSecureIntForUser(
902                     mAudioService.getContentResolver(),
903                     Settings.Secure.AUDIO_SAFE_CSD_AS_A_FEATURE_ENABLED, 0,
904                     UserHandle.USER_CURRENT) != 0;
905         }
906 
907         synchronized (mCsdStateLock) {
908             if (mGlobalTimeOffsetInSecs == GLOBAL_TIME_OFFSET_UNINITIALIZED) {
909                 mGlobalTimeOffsetInSecs = System.currentTimeMillis() / 1000L;
910             }
911 
912             float prevCsd = mCurrentCsd;
913             // Restore persisted values
914             mCurrentCsd = parseGlobalSettingFloat(
915                     Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE, /* defaultValue= */0.f);
916             if (mCurrentCsd != prevCsd) {
917                 mNextCsdWarning = parseGlobalSettingFloat(
918                         Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING, /* defaultValue= */1.f);
919                 final List<SoundDoseRecord> records = persistedStringToRecordList(
920                         mSettings.getGlobalString(mAudioService.getContentResolver(),
921                                 Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS),
922                         mGlobalTimeOffsetInSecs);
923                 if (records != null) {
924                     mDoseRecords.addAll(records);
925                 }
926             }
927         }
928 
929         reset();
930     }
931 
onConfigureSafeMedia(boolean force, String caller)932     private void onConfigureSafeMedia(boolean force, String caller) {
933         updateCsdEnabled(caller);
934 
935         synchronized (mSafeMediaVolumeStateLock) {
936             int mcc = mContext.getResources().getConfiguration().mcc;
937             if ((mMcc != mcc) || ((mMcc == 0) && force)) {
938                 mSafeMediaVolumeIndex = mContext.getResources().getInteger(
939                         com.android.internal.R.integer.config_safe_media_volume_index) * 10;
940                 initSafeMediaVolumeIndex();
941 
942                 updateSafeMediaVolume_l(caller);
943 
944                 mMcc = mcc;
945             }
946         }
947     }
948 
949     @GuardedBy("mSafeMediaVolumeStateLock")
updateSafeMediaVolume_l(String caller)950     private void updateSafeMediaVolume_l(String caller) {
951         boolean safeMediaVolumeBypass =
952                 SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_BYPASS, false)
953                         || mEnableCsd.get();
954         boolean safeMediaVolumeForce = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_FORCE,
955                 false);
956         // we are using the MCC overlaid legacy flag used for the safe volume enablement
957         // to determine whether the MCC enforces any safe hearing standard.
958         boolean mccEnforcedSafeMediaVolume = mContext.getResources().getBoolean(
959                 com.android.internal.R.bool.config_safe_media_volume_enabled);
960 
961         boolean safeVolumeEnabled =
962                 (mccEnforcedSafeMediaVolume || safeMediaVolumeForce) && !safeMediaVolumeBypass;
963 
964         // The persisted state is either "disabled" or "active": this is the state applied
965         // next time we boot and cannot be "inactive"
966         int persistedState;
967         if (safeVolumeEnabled) {
968             persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
969             // The state can already be "inactive" here if the user has forced it before
970             // the 30 seconds timeout for forced configuration. In this case we don't reset
971             // it to "active".
972             if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
973                 if (mMusicActiveMs == 0) {
974                     mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
975                     enforceSafeMediaVolume(caller);
976                 } else {
977                     // We have existing playback time recorded, already confirmed.
978                     mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
979                     mLastMusicActiveTimeMs = 0;
980                 }
981             }
982         } else {
983             persistedState = SAFE_MEDIA_VOLUME_DISABLED;
984             mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
985         }
986 
987         mAudioHandler.sendMessageAtTime(
988                 mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE,
989                         persistedState, /*arg2=*/0,
990                         /*obj=*/null), /*delay=*/0);
991     }
992 
updateCsdEnabled(String caller)993     private void updateCsdEnabled(String caller) {
994         boolean csdForce = SystemProperties.getBoolean(SYSTEM_PROPERTY_SAFEMEDIA_CSD_FORCE, false);
995         // we are using the MCC overlaid legacy flag used for the safe volume enablement
996         // to determine whether the MCC enforces any safe hearing standard.
997         boolean mccEnforcedSafeMedia = mContext.getResources().getBoolean(
998                 com.android.internal.R.bool.config_safe_media_volume_enabled);
999         boolean csdEnable = mContext.getResources().getBoolean(
1000                 R.bool.config_safe_sound_dosage_enabled);
1001         boolean newEnabledCsd = (mccEnforcedSafeMedia && csdEnable) || csdForce;
1002 
1003         synchronized (mCsdAsAFeatureLock) {
1004             if (!mccEnforcedSafeMedia && csdEnable) {
1005                 mIsCsdAsAFeatureAvailable = true;
1006                 newEnabledCsd = mIsCsdAsAFeatureEnabled || csdForce;
1007                 Log.v(TAG, caller + ": CSD as a feature is not enforced and enabled: "
1008                         + newEnabledCsd);
1009             } else {
1010                 mIsCsdAsAFeatureAvailable = false;
1011             }
1012         }
1013 
1014         if (mEnableCsd.compareAndSet(!newEnabledCsd, newEnabledCsd)) {
1015             Log.i(TAG, caller + ": enabled CSD " + newEnabledCsd);
1016             initCsd();
1017 
1018             synchronized (mSafeMediaVolumeStateLock) {
1019                 initSafeMediaVolumeIndex();
1020                 updateSafeMediaVolume_l(caller);
1021             }
1022         }
1023     }
1024 
getTimeoutMsForWarning(@udioManager.CsdWarning int csdWarning)1025     private int getTimeoutMsForWarning(@AudioManager.CsdWarning int csdWarning) {
1026         switch (csdWarning) {
1027             case AudioManager.CSD_WARNING_DOSE_REACHED_1X:
1028                 return CSD_WARNING_TIMEOUT_MS_DOSE_1X;
1029             case AudioManager.CSD_WARNING_DOSE_REPEATED_5X:
1030                 return CSD_WARNING_TIMEOUT_MS_DOSE_5X;
1031             case AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE:
1032                 return CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE;
1033             case AudioManager.CSD_WARNING_ACCUMULATION_START:
1034                 return CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START;
1035         }
1036         Log.e(TAG, "Invalid CSD warning " + csdWarning, new Exception());
1037         return -1;
1038     }
1039 
1040     @GuardedBy("mSafeMediaVolumeStateLock")
setSafeMediaVolumeEnabled(boolean on, String caller)1041     private void setSafeMediaVolumeEnabled(boolean on, String caller) {
1042         if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) && (mSafeMediaVolumeState
1043                 != SAFE_MEDIA_VOLUME_DISABLED)) {
1044             if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) {
1045                 mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
1046                 enforceSafeMediaVolume(caller);
1047             } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
1048                 mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
1049                 mMusicActiveMs = 1;  // nonzero = confirmed
1050                 mLastMusicActiveTimeMs = 0;
1051                 saveMusicActiveMs();
1052                 scheduleMusicActiveCheck();
1053             }
1054         }
1055     }
1056 
1057     @GuardedBy("mSafeMediaVolumeStateLock")
cancelMusicActiveCheck()1058     private void cancelMusicActiveCheck() {
1059         if (mMusicActiveIntent != null) {
1060             mAlarmManager.cancel(mMusicActiveIntent);
1061             mMusicActiveIntent = null;
1062         }
1063     }
1064 
1065     @GuardedBy("mSafeMediaVolumeStateLock")
saveMusicActiveMs()1066     private void saveMusicActiveMs() {
1067         mAudioHandler.obtainMessage(MSG_PERSIST_MUSIC_ACTIVE_MS, mMusicActiveMs, 0).sendToTarget();
1068     }
1069 
getSafeDeviceMediaVolumeIndex(int deviceType)1070     private int getSafeDeviceMediaVolumeIndex(int deviceType) {
1071         if (!mEnableCsd.get()) {
1072             // legacy hearing safety only for wired and USB HS/HP
1073             if (deviceType == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE
1074                     || deviceType == AudioSystem.DEVICE_OUT_WIRED_HEADSET) {
1075                 // legacy hearing safety uses mSafeMediaVolumeIndex for wired HS/HP
1076                 // instead of computing it from the volume curves
1077                 return mSafeMediaVolumeIndex;
1078             }
1079 
1080             if (deviceType != AudioSystem.DEVICE_OUT_USB_HEADSET) {
1081                 return SAFE_MEDIA_VOLUME_UNINITIALIZED;
1082             }
1083         }
1084 
1085         // determine UI volume index corresponding to the wanted safe gain in dBFS
1086         int min = MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
1087         int max = MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
1088 
1089         mSafeMediaVolumeDbfs = mContext.getResources().getInteger(
1090                 com.android.internal.R.integer.config_safe_media_volume_usb_mB) / 100.0f;
1091 
1092         while (Math.abs(max - min) > 1) {
1093             int index = (max + min) / 2;
1094             float gainDB = AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC, index,
1095                     deviceType);
1096             if (Float.isNaN(gainDB)) {
1097                 //keep last min in case of read error
1098                 break;
1099             } else if (gainDB == mSafeMediaVolumeDbfs) {
1100                 min = index;
1101                 break;
1102             } else if (gainDB < mSafeMediaVolumeDbfs) {
1103                 min = index;
1104             } else {
1105                 max = index;
1106             }
1107         }
1108         return min * 10;
1109     }
1110 
onPersistSafeVolumeState(int state)1111     private void onPersistSafeVolumeState(int state) {
1112         mSettings.putGlobalInt(mAudioService.getContentResolver(),
1113                 Settings.Global.AUDIO_SAFE_VOLUME_STATE,
1114                 state);
1115     }
1116 
safeMediaVolumeStateToString(int state)1117     private static String safeMediaVolumeStateToString(int state) {
1118         switch(state) {
1119             case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED";
1120             case SAFE_MEDIA_VOLUME_DISABLED: return "SAFE_MEDIA_VOLUME_DISABLED";
1121             case SAFE_MEDIA_VOLUME_INACTIVE: return "SAFE_MEDIA_VOLUME_INACTIVE";
1122             case SAFE_MEDIA_VOLUME_ACTIVE: return "SAFE_MEDIA_VOLUME_ACTIVE";
1123         }
1124         return null;
1125     }
1126 
1127     @GuardedBy("mCsdStateLock")
updateSoundDoseRecords_l(SoundDoseRecord[] newRecords, float currentCsd)1128     private void updateSoundDoseRecords_l(SoundDoseRecord[] newRecords, float currentCsd) {
1129         long totalDuration = 0;
1130         for (SoundDoseRecord record : newRecords) {
1131             Log.i(TAG, "  new record: " + record);
1132             totalDuration += record.duration;
1133 
1134             if (record.value < 0) {
1135                 // Negative value means the record timestamp exceeded the CSD rolling window size
1136                 // and needs to be removed
1137                 if (!mDoseRecords.removeIf(
1138                         r -> r.value == -record.value && r.timestamp == record.timestamp
1139                                 && r.averageMel == record.averageMel
1140                                 && r.duration == record.duration)) {
1141                     Log.w(TAG, "Could not find cached record to remove: " + record);
1142                 }
1143             } else {
1144                 mDoseRecords.add(record);
1145             }
1146         }
1147 
1148         mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
1149                 /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0);
1150 
1151         mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
1152     }
1153 
1154     @SuppressWarnings("GuardedBy")  // avoid limitation with intra-procedural analysis of lambdas
onPersistSoundDoseRecords()1155     private void onPersistSoundDoseRecords() {
1156         synchronized (mCsdStateLock) {
1157             if (mGlobalTimeOffsetInSecs == GLOBAL_TIME_OFFSET_UNINITIALIZED) {
1158                 mGlobalTimeOffsetInSecs = System.currentTimeMillis() / 1000L;
1159             }
1160 
1161             mSettings.putGlobalString(mAudioService.getContentResolver(),
1162                     Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
1163                     Float.toString(mCurrentCsd));
1164             mSettings.putGlobalString(mAudioService.getContentResolver(),
1165                     Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
1166                     Float.toString(mNextCsdWarning));
1167             mSettings.putGlobalString(mAudioService.getContentResolver(),
1168                     Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
1169                     mDoseRecords.stream().map(
1170                             record -> SoundDoseHelper.recordToPersistedString(record,
1171                                     mGlobalTimeOffsetInSecs)).collect(
1172                             Collectors.joining(PERSIST_CSD_RECORD_SEPARATOR_CHAR)));
1173         }
1174     }
1175 
recordToPersistedString(SoundDoseRecord record, long globalTimeOffsetInSecs)1176     private static String recordToPersistedString(SoundDoseRecord record,
1177             long globalTimeOffsetInSecs) {
1178         return convertToGlobalTime(record.timestamp, globalTimeOffsetInSecs)
1179                 + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.duration
1180                 + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.value
1181                 + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.averageMel;
1182     }
1183 
convertToGlobalTime(long bootTimeInSecs, long globalTimeOffsetInSecs)1184     private static long convertToGlobalTime(long bootTimeInSecs, long globalTimeOffsetInSecs) {
1185         return bootTimeInSecs + globalTimeOffsetInSecs;
1186     }
1187 
convertToBootTime(long globalTimeInSecs, long globalTimeOffsetInSecs)1188     private static long convertToBootTime(long globalTimeInSecs, long globalTimeOffsetInSecs) {
1189         return globalTimeInSecs - globalTimeOffsetInSecs;
1190     }
1191 
persistedStringToRecordList(String records, long globalTimeOffsetInSecs)1192     private static List<SoundDoseRecord> persistedStringToRecordList(String records,
1193             long globalTimeOffsetInSecs) {
1194         if (records == null || records.isEmpty()) {
1195             return null;
1196         }
1197         return Arrays.stream(TextUtils.split(records, PERSIST_CSD_RECORD_SEPARATOR)).map(
1198                 record -> SoundDoseHelper.persistedStringToRecord(record,
1199                         globalTimeOffsetInSecs)).filter(Objects::nonNull).collect(
1200                 Collectors.toList());
1201     }
1202 
persistedStringToRecord(String record, long globalTimeOffsetInSecs)1203     private static SoundDoseRecord persistedStringToRecord(String record,
1204             long globalTimeOffsetInSecs) {
1205         if (record == null || record.isEmpty()) {
1206             return null;
1207         }
1208         final String[] fields = TextUtils.split(record, PERSIST_CSD_RECORD_FIELD_SEPARATOR);
1209         if (fields.length != 4) {
1210             Log.w(TAG, "Expecting 4 fields for a SoundDoseRecord, parsed " + fields.length);
1211             return null;
1212         }
1213 
1214         final SoundDoseRecord sdRecord = new SoundDoseRecord();
1215         try {
1216             sdRecord.timestamp = convertToBootTime(Long.parseLong(fields[0]),
1217                     globalTimeOffsetInSecs);
1218             sdRecord.duration = Integer.parseInt(fields[1]);
1219             sdRecord.value = Float.parseFloat(fields[2]);
1220             sdRecord.averageMel = Float.parseFloat(fields[3]);
1221         } catch (NumberFormatException e) {
1222             Log.e(TAG, "Unable to parse persisted SoundDoseRecord: " + record, e);
1223             return null;
1224         }
1225 
1226         return sdRecord;
1227     }
1228 
parseGlobalSettingFloat(String audioSafeCsdCurrentValue, float defaultValue)1229     private float parseGlobalSettingFloat(String audioSafeCsdCurrentValue, float defaultValue) {
1230         String stringValue = mSettings.getGlobalString(mAudioService.getContentResolver(),
1231                 audioSafeCsdCurrentValue);
1232         if (stringValue == null || stringValue.isEmpty()) {
1233             Log.v(TAG, "No value stored in settings " + audioSafeCsdCurrentValue);
1234             return defaultValue;
1235         }
1236 
1237         float value;
1238         try {
1239             value = Float.parseFloat(stringValue);
1240         } catch (NumberFormatException e) {
1241             Log.e(TAG, "Error parsing float from settings " + audioSafeCsdCurrentValue, e);
1242             value = defaultValue;
1243         }
1244 
1245         return value;
1246     }
1247 
1248     // StreamVolumeCommand contains the information needed to defer the process of
1249     // setStreamVolume() in case the user has to acknowledge the safe volume warning message.
1250     private static class StreamVolumeCommand {
1251         public final int mStreamType;
1252         public final int mIndex;
1253         public final int mFlags;
1254         public final int mDevice;
1255 
StreamVolumeCommand(int streamType, int index, int flags, int device)1256         StreamVolumeCommand(int streamType, int index, int flags, int device) {
1257             mStreamType = streamType;
1258             mIndex = index;
1259             mFlags = flags;
1260             mDevice = device;
1261         }
1262 
1263         @Override
toString()1264         public String toString() {
1265             return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=")
1266                     .append(mIndex).append(",flags=").append(mFlags).append(",device=")
1267                     .append(mDevice).append('}').toString();
1268         }
1269     }
1270 }
1271