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