| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.audio; |
| |
| import static com.android.server.audio.AudioService.MAX_STREAM_VOLUME; |
| import static com.android.server.audio.AudioService.MIN_STREAM_VOLUME; |
| import static com.android.server.audio.AudioService.MSG_SET_DEVICE_VOLUME; |
| import static com.android.server.audio.AudioService.SAFE_MEDIA_VOLUME_MSG_START; |
| |
| import android.annotation.NonNull; |
| import android.app.AlarmManager; |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.media.AudioManager; |
| import android.media.AudioSystem; |
| import android.media.ISoundDose; |
| import android.media.ISoundDoseCallback; |
| import android.media.SoundDoseRecord; |
| import android.os.Binder; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.MathUtils; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.server.audio.AudioService.AudioHandler; |
| import com.android.server.audio.AudioService.ISafeHearingVolumeController; |
| import com.android.server.audio.AudioServiceEvents.SoundDoseEvent; |
| import com.android.server.utils.EventLogger; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Safe media volume management. |
| * MUSIC stream volume level is limited when headphones are connected according to safety |
| * regulation. When the user attempts to raise the volume above the limit, a warning is |
| * displayed and the user has to acknowledge before the volume is actually changed. |
| * The volume index corresponding to the limit is stored in config_safe_media_volume_index |
| * property. Platforms with a different limit must set this property accordingly in their |
| * overlay. |
| */ |
| public class SoundDoseHelper { |
| private static final String TAG = "AS.SoundDoseHelper"; |
| |
| /*package*/ static final String ACTION_CHECK_MUSIC_ACTIVE = |
| "com.android.server.audio.action.CHECK_MUSIC_ACTIVE"; |
| |
| // mSafeMediaVolumeState indicates whether the media volume is limited over headphones. |
| // It is SAFE_MEDIA_VOLUME_NOT_CONFIGURED at boot time until a network service is connected |
| // or the configure time is elapsed. It is then set to SAFE_MEDIA_VOLUME_ACTIVE or |
| // SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it |
| // can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume() |
| // (when user opts out). |
| // Note: when CSD calculation is enabled the state is set to SAFE_MEDIA_VOLUME_DISABLED |
| private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0; |
| private static final int SAFE_MEDIA_VOLUME_DISABLED = 1; |
| private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2; // confirmed |
| private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3; // unconfirmed |
| |
| private static final int MSG_CONFIGURE_SAFE_MEDIA = SAFE_MEDIA_VOLUME_MSG_START + 1; |
| private static final int MSG_PERSIST_SAFE_VOLUME_STATE = SAFE_MEDIA_VOLUME_MSG_START + 2; |
| private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = SAFE_MEDIA_VOLUME_MSG_START + 3; |
| private static final int MSG_PERSIST_CSD_VALUES = SAFE_MEDIA_VOLUME_MSG_START + 4; |
| |
| private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours |
| |
| // 30s after boot completed |
| private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000; |
| |
| private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000; // 1 minute polling interval |
| private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1; |
| |
| // timeouts for the CSD warnings, -1 means no timeout (dialog must be ack'd by user) |
| private static final int CSD_WARNING_TIMEOUT_MS_DOSE_1X = 7000; |
| private static final int CSD_WARNING_TIMEOUT_MS_DOSE_5X = 5000; |
| private static final int CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START = -1; |
| private static final int CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE = 5000; |
| |
| private static final String PERSIST_CSD_RECORD_FIELD_SEPARATOR = ","; |
| private static final String PERSIST_CSD_RECORD_SEPARATOR_CHAR = "|"; |
| private static final String PERSIST_CSD_RECORD_SEPARATOR = "\\|"; |
| |
| private static final long GLOBAL_TIME_OFFSET_UNINITIALIZED = -1; |
| |
| private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE, |
| "CSD updates"); |
| |
| private int mMcc = 0; |
| |
| private final Object mSafeMediaVolumeStateLock = new Object(); |
| private int mSafeMediaVolumeState; |
| |
| // Used when safe volume warning message display is requested by setStreamVolume(). In this |
| // case, the new requested volume, stream type and device are stored in mPendingVolumeCommand |
| // and used later when/if disableSafeMediaVolume() is called. |
| private StreamVolumeCommand mPendingVolumeCommand; |
| |
| // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property |
| private int mSafeMediaVolumeIndex; |
| // mSafeUsbMediaVolumeDbfs is the cached value of the config_safe_media_volume_usb_mB |
| // property, divided by 100.0. |
| private float mSafeUsbMediaVolumeDbfs; |
| |
| // mSafeUsbMediaVolumeIndex is used for USB Headsets and is the music volume UI index |
| // corresponding to a gain of mSafeUsbMediaVolumeDbfs (defaulting to -37dB) in audio |
| // flinger mixer. |
| // We remove -22 dBs from the theoretical -15dB to account for the EQ + bass boost |
| // amplification when both effects are on with all band gains at maximum. |
| // This level corresponds to a loudness of 85 dB SPL for the warning to be displayed when |
| // the headset is compliant to EN 60950 with a max loudness of 100dB SPL. |
| private int mSafeUsbMediaVolumeIndex; |
| // mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced, |
| private final Set<Integer> mSafeMediaVolumeDevices = new HashSet<>( |
| Arrays.asList(AudioSystem.DEVICE_OUT_WIRED_HEADSET, |
| AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, AudioSystem.DEVICE_OUT_USB_HEADSET)); |
| |
| |
| // mMusicActiveMs is the cumulative time of music activity since safe volume was disabled. |
| // When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled |
| // automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS. |
| private int mMusicActiveMs; |
| private long mLastMusicActiveTimeMs = 0; |
| private PendingIntent mMusicActiveIntent = null; |
| private final AlarmManager mAlarmManager; |
| |
| @NonNull private final AudioService mAudioService; |
| @NonNull private final SettingsAdapter mSettings; |
| @NonNull private final AudioHandler mAudioHandler; |
| @NonNull private final ISafeHearingVolumeController mVolumeController; |
| |
| private final boolean mEnableCsd; |
| |
| private ISoundDose mSoundDose; |
| |
| private final Object mCsdStateLock = new Object(); |
| |
| @GuardedBy("mCsdStateLock") |
| private float mCurrentCsd = 0.f; |
| // dose at which the next dose reached warning occurs |
| @GuardedBy("mCsdStateLock") |
| private float mNextCsdWarning = 1.0f; |
| @GuardedBy("mCsdStateLock") |
| private final List<SoundDoseRecord> mDoseRecords = new ArrayList<>(); |
| |
| // time in seconds reported by System.currentTimeInMillis used as an offset to convert between |
| // boot time and global time |
| @GuardedBy("mCsdStateLock") |
| private long mGlobalTimeOffsetInSecs = GLOBAL_TIME_OFFSET_UNINITIALIZED; |
| |
| private final Context mContext; |
| |
| private final ISoundDoseCallback.Stub mSoundDoseCallback = new ISoundDoseCallback.Stub() { |
| public void onMomentaryExposure(float currentMel, int deviceId) { |
| Log.w(TAG, "DeviceId " + deviceId + " triggered momentary exposure with value: " |
| + currentMel); |
| mLogger.enqueue(SoundDoseEvent.getMomentaryExposureEvent(currentMel)); |
| if (mEnableCsd) { |
| mVolumeController.postDisplayCsdWarning( |
| AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE, |
| getTimeoutMsForWarning(AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE)); |
| } |
| } |
| |
| public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) { |
| if (!mEnableCsd) { |
| Log.w(TAG, "onNewCsdValue: csd not supported, ignoring value"); |
| return; |
| } |
| |
| Log.i(TAG, "onNewCsdValue: " + currentCsd); |
| synchronized (mCsdStateLock) { |
| if (mCurrentCsd < currentCsd) { |
| // dose increase: going over next threshold? |
| if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) { |
| if (mNextCsdWarning == 5.0f) { |
| // 500% dose repeat |
| mVolumeController.postDisplayCsdWarning( |
| AudioManager.CSD_WARNING_DOSE_REPEATED_5X, |
| getTimeoutMsForWarning( |
| AudioManager.CSD_WARNING_DOSE_REPEATED_5X)); |
| // on the 5x dose warning, the volume reduction happens right away |
| mAudioService.postLowerVolumeToRs1(); |
| } else { |
| mVolumeController.postDisplayCsdWarning( |
| AudioManager.CSD_WARNING_DOSE_REACHED_1X, |
| getTimeoutMsForWarning( |
| AudioManager.CSD_WARNING_DOSE_REACHED_1X)); |
| } |
| mNextCsdWarning += 1.0f; |
| } |
| } else { |
| // dose decrease: dropping below previous threshold of warning? |
| if ((currentCsd < mNextCsdWarning - 1.0f) && ( |
| mNextCsdWarning >= 2.0f)) { |
| mNextCsdWarning -= 1.0f; |
| } |
| } |
| mCurrentCsd = currentCsd; |
| updateSoundDoseRecords_l(records, currentCsd); |
| } |
| } |
| }; |
| |
| SoundDoseHelper(@NonNull AudioService audioService, Context context, |
| @NonNull AudioHandler audioHandler, |
| @NonNull SettingsAdapter settings, |
| @NonNull ISafeHearingVolumeController volumeController) { |
| mAudioService = audioService; |
| mAudioHandler = audioHandler; |
| mSettings = settings; |
| mVolumeController = volumeController; |
| |
| mContext = context; |
| |
| mEnableCsd = mContext.getResources().getBoolean(R.bool.config_audio_csd_enabled_default); |
| if (mEnableCsd) { |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED; |
| } else { |
| mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0); |
| } |
| |
| // The default safe volume index read here will be replaced by the actual value when |
| // the mcc is read by onConfigureSafeMedia() |
| // For now we use the same index for RS2 initial warning with CSD |
| mSafeMediaVolumeIndex = mContext.getResources().getInteger( |
| R.integer.config_safe_media_volume_index) * 10; |
| |
| mAlarmManager = (AlarmManager) mContext.getSystemService( |
| Context.ALARM_SERVICE); |
| } |
| |
| float getRs2Value() { |
| if (!mEnableCsd) { |
| return 0.f; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| return mSoundDose.getOutputRs2(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while getting the RS2 exposure value", e); |
| return 0.f; |
| } |
| } |
| |
| void setRs2Value(float rs2Value) { |
| if (!mEnableCsd) { |
| return; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| mSoundDose.setOutputRs2(rs2Value); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while setting the RS2 exposure value", e); |
| } |
| } |
| |
| float getCsd() { |
| if (!mEnableCsd) { |
| return -1.f; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| return mSoundDose.getCsd(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while getting the CSD value", e); |
| return -1.f; |
| } |
| } |
| |
| void setCsd(float csd) { |
| if (!mEnableCsd) { |
| return; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| final SoundDoseRecord record = new SoundDoseRecord(); |
| record.timestamp = System.currentTimeMillis(); |
| record.value = csd; |
| final SoundDoseRecord[] recordArray = new SoundDoseRecord[] { record }; |
| mSoundDose.resetCsd(csd, recordArray); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while setting the CSD value", e); |
| } |
| } |
| |
| void forceUseFrameworkMel(boolean useFrameworkMel) { |
| if (!mEnableCsd) { |
| return; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| mSoundDose.forceUseFrameworkMel(useFrameworkMel); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while forcing the internal MEL computation", e); |
| } |
| } |
| |
| void forceComputeCsdOnAllDevices(boolean computeCsdOnAllDevices) { |
| if (!mEnableCsd) { |
| return; |
| } |
| |
| Objects.requireNonNull(mSoundDose, "Sound dose interface not initialized"); |
| try { |
| mSoundDose.forceComputeCsdOnAllDevices(computeCsdOnAllDevices); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while forcing CSD computation on all devices", e); |
| } |
| } |
| |
| boolean isCsdEnabled() { |
| return mEnableCsd; |
| } |
| |
| /*package*/ int safeMediaVolumeIndex(int device) { |
| if (!mSafeMediaVolumeDevices.contains(device)) { |
| return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]; |
| } |
| if (device == AudioSystem.DEVICE_OUT_USB_HEADSET) { |
| return mSafeUsbMediaVolumeIndex; |
| } else { |
| return mSafeMediaVolumeIndex; |
| } |
| } |
| |
| /*package*/ void restoreMusicActiveMs() { |
| synchronized (mSafeMediaVolumeStateLock) { |
| mMusicActiveMs = MathUtils.constrain( |
| mSettings.getSecureIntForUser(mAudioService.getContentResolver(), |
| Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, 0, |
| UserHandle.USER_CURRENT), |
| 0, UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX); |
| } |
| } |
| |
| /*package*/ void enforceSafeMediaVolumeIfActive(String caller) { |
| synchronized (mSafeMediaVolumeStateLock) { |
| if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) { |
| enforceSafeMediaVolume(caller); |
| } |
| } |
| } |
| |
| /*package*/ void enforceSafeMediaVolume(String caller) { |
| AudioService.VolumeStreamState streamState = mAudioService.getVssVolumeForStream( |
| AudioSystem.STREAM_MUSIC); |
| Set<Integer> devices = mSafeMediaVolumeDevices; |
| |
| for (int device : devices) { |
| int index = streamState.getIndex(device); |
| int safeIndex = safeMediaVolumeIndex(device); |
| if (index > safeIndex) { |
| streamState.setIndex(safeIndex, device, caller, true /*hasModifyAudioSettings*/); |
| mAudioHandler.sendMessageAtTime( |
| mAudioHandler.obtainMessage(MSG_SET_DEVICE_VOLUME, device, /*arg2=*/0, |
| streamState), /*delay=*/0); |
| } |
| } |
| } |
| |
| /*package*/ boolean checkSafeMediaVolume(int streamType, int index, int device) { |
| boolean result; |
| synchronized (mSafeMediaVolumeStateLock) { |
| result = checkSafeMediaVolume_l(streamType, index, device); |
| } |
| return result; |
| } |
| |
| @GuardedBy("mSafeMediaVolumeStateLock") |
| private boolean checkSafeMediaVolume_l(int streamType, int index, int device) { |
| return (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_ACTIVE) |
| || (AudioService.mStreamVolumeAlias[streamType] != AudioSystem.STREAM_MUSIC) |
| || (!mSafeMediaVolumeDevices.contains(device)) |
| || (index <= safeMediaVolumeIndex(device)) |
| || mEnableCsd; |
| } |
| |
| /*package*/ boolean willDisplayWarningAfterCheckVolume(int streamType, int index, int device, |
| int flags) { |
| synchronized (mSafeMediaVolumeStateLock) { |
| if (!checkSafeMediaVolume_l(streamType, index, device)) { |
| mVolumeController.postDisplaySafeVolumeWarning(flags); |
| mPendingVolumeCommand = new StreamVolumeCommand( |
| streamType, index, flags, device); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /*package*/ void disableSafeMediaVolume(String callingPackage) { |
| synchronized (mSafeMediaVolumeStateLock) { |
| final long identity = Binder.clearCallingIdentity(); |
| setSafeMediaVolumeEnabled(false, callingPackage); |
| Binder.restoreCallingIdentity(identity); |
| |
| if (mPendingVolumeCommand != null) { |
| mAudioService.onSetStreamVolume(mPendingVolumeCommand.mStreamType, |
| mPendingVolumeCommand.mIndex, |
| mPendingVolumeCommand.mFlags, |
| mPendingVolumeCommand.mDevice, |
| callingPackage, true /*hasModifyAudioSettings*/, |
| true /*canChangeMute*/); |
| mPendingVolumeCommand = null; |
| } |
| } |
| } |
| |
| /*package*/ void scheduleMusicActiveCheck() { |
| synchronized (mSafeMediaVolumeStateLock) { |
| cancelMusicActiveCheck(); |
| if (!mEnableCsd) { |
| mMusicActiveIntent = PendingIntent.getBroadcast(mContext, |
| REQUEST_CODE_CHECK_MUSIC_ACTIVE, |
| new Intent(ACTION_CHECK_MUSIC_ACTIVE), |
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); |
| mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, |
| SystemClock.elapsedRealtime() |
| + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent); |
| } |
| } |
| } |
| |
| /*package*/ void onCheckMusicActive(String caller, boolean isStreamActive) { |
| synchronized (mSafeMediaVolumeStateLock) { |
| if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) { |
| int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC); |
| if (mSafeMediaVolumeDevices.contains(device) && isStreamActive) { |
| scheduleMusicActiveCheck(); |
| int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC, |
| device); |
| if (index > safeMediaVolumeIndex(device)) { |
| // Approximate cumulative active music time |
| long curTimeMs = SystemClock.elapsedRealtime(); |
| if (mLastMusicActiveTimeMs != 0) { |
| mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs); |
| } |
| mLastMusicActiveTimeMs = curTimeMs; |
| Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs); |
| if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) { |
| setSafeMediaVolumeEnabled(true, caller); |
| mMusicActiveMs = 0; |
| } |
| saveMusicActiveMs(); |
| } |
| } else { |
| cancelMusicActiveCheck(); |
| mLastMusicActiveTimeMs = 0; |
| } |
| } |
| } |
| } |
| |
| /*package*/ void configureSafeMedia(boolean forced, String caller) { |
| int msg = MSG_CONFIGURE_SAFE_MEDIA; |
| mAudioHandler.removeMessages(msg); |
| |
| long time = 0; |
| if (forced) { |
| time = (SystemClock.uptimeMillis() + (SystemProperties.getBoolean( |
| "audio.safemedia.bypass", false) ? 0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS)); |
| } |
| mAudioHandler.sendMessageAtTime( |
| mAudioHandler.obtainMessage(msg, /*arg1=*/forced ? 1 : 0, /*arg2=*/0, caller), |
| time); |
| } |
| |
| /*package*/ void initSafeUsbMediaVolumeIndex() { |
| // mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it |
| // relies on audio policy having correct ranges for volume indexes. |
| mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex(); |
| } |
| |
| /*package*/ int getSafeMediaVolumeIndex(int device) { |
| if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE && mSafeMediaVolumeDevices.contains( |
| device)) { |
| return safeMediaVolumeIndex(device); |
| } else { |
| return -1; |
| } |
| } |
| |
| /*package*/ boolean raiseVolumeDisplaySafeMediaVolume(int streamType, int index, int device, |
| int flags) { |
| if (checkSafeMediaVolume(streamType, index, device)) { |
| return false; |
| } |
| |
| mVolumeController.postDisplaySafeVolumeWarning(flags); |
| return true; |
| } |
| |
| /*package*/ boolean safeDevicesContains(int device) { |
| return mSafeMediaVolumeDevices.contains(device); |
| } |
| |
| /*package*/ void invalidatPendingVolumeCommand() { |
| synchronized (mSafeMediaVolumeStateLock) { |
| mPendingVolumeCommand = null; |
| } |
| } |
| |
| /*package*/ void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_CONFIGURE_SAFE_MEDIA: |
| onConfigureSafeMedia((msg.arg1 == 1), (String) msg.obj); |
| break; |
| case MSG_PERSIST_SAFE_VOLUME_STATE: |
| onPersistSafeVolumeState(msg.arg1); |
| break; |
| case MSG_PERSIST_MUSIC_ACTIVE_MS: |
| final int musicActiveMs = msg.arg1; |
| mSettings.putSecureIntForUser(mAudioService.getContentResolver(), |
| Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs, |
| UserHandle.USER_CURRENT); |
| break; |
| case MSG_PERSIST_CSD_VALUES: |
| onPersistSoundDoseRecords(); |
| break; |
| default: |
| Log.e(TAG, "Unexpected msg to handle: " + msg.what); |
| break; |
| } |
| } |
| |
| /*package*/ void dump(PrintWriter pw) { |
| pw.print(" mEnableCsd="); pw.println(mEnableCsd); |
| pw.print(" mSafeMediaVolumeState="); |
| pw.println(safeMediaVolumeStateToString(mSafeMediaVolumeState)); |
| pw.print(" mSafeMediaVolumeIndex="); pw.println(mSafeMediaVolumeIndex); |
| pw.print(" mSafeUsbMediaVolumeIndex="); pw.println(mSafeUsbMediaVolumeIndex); |
| pw.print(" mSafeUsbMediaVolumeDbfs="); pw.println(mSafeUsbMediaVolumeDbfs); |
| pw.print(" mMusicActiveMs="); pw.println(mMusicActiveMs); |
| pw.print(" mMcc="); pw.println(mMcc); |
| pw.print(" mPendingVolumeCommand="); pw.println(mPendingVolumeCommand); |
| pw.println(); |
| mLogger.dump(pw); |
| pw.println(); |
| } |
| |
| /*package*/void reset() { |
| Log.d(TAG, "Reset the sound dose helper"); |
| mSoundDose = AudioSystem.getSoundDoseInterface(mSoundDoseCallback); |
| synchronized (mCsdStateLock) { |
| try { |
| if (mSoundDose != null && mSoundDose.asBinder().isBinderAlive()) { |
| if (mCurrentCsd != 0.f) { |
| Log.d(TAG, |
| "Resetting the saved sound dose value " + mCurrentCsd); |
| SoundDoseRecord[] records = mDoseRecords.toArray( |
| new SoundDoseRecord[0]); |
| mSoundDose.resetCsd(mCurrentCsd, records); |
| } |
| } |
| } catch (RemoteException e) { |
| // noop |
| } |
| } |
| } |
| |
| private void initCsd() { |
| if (mEnableCsd) { |
| Log.v(TAG, "Initializing sound dose"); |
| |
| synchronized (mCsdStateLock) { |
| if (mGlobalTimeOffsetInSecs == GLOBAL_TIME_OFFSET_UNINITIALIZED) { |
| mGlobalTimeOffsetInSecs = System.currentTimeMillis() / 1000L; |
| } |
| |
| float prevCsd = mCurrentCsd; |
| // Restore persisted values |
| mCurrentCsd = parseGlobalSettingFloat( |
| Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE, /* defaultValue= */0.f); |
| if (mCurrentCsd != prevCsd) { |
| mNextCsdWarning = parseGlobalSettingFloat( |
| Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING, /* defaultValue= */1.f); |
| final List<SoundDoseRecord> records = persistedStringToRecordList( |
| mSettings.getGlobalString(mAudioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS), |
| mGlobalTimeOffsetInSecs); |
| if (records != null) { |
| mDoseRecords.addAll(records); |
| } |
| } |
| } |
| |
| reset(); |
| } |
| } |
| |
| private void onConfigureSafeMedia(boolean force, String caller) { |
| synchronized (mSafeMediaVolumeStateLock) { |
| int mcc = mContext.getResources().getConfiguration().mcc; |
| if ((mMcc != mcc) || ((mMcc == 0) && force)) { |
| mSafeMediaVolumeIndex = mContext.getResources().getInteger( |
| com.android.internal.R.integer.config_safe_media_volume_index) * 10; |
| |
| mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex(); |
| |
| boolean safeMediaVolumeEnabled = |
| SystemProperties.getBoolean("audio.safemedia.force", false) |
| || mContext.getResources().getBoolean( |
| com.android.internal.R.bool.config_safe_media_volume_enabled); |
| |
| boolean safeMediaVolumeBypass = |
| SystemProperties.getBoolean("audio.safemedia.bypass", false); |
| |
| // The persisted state is either "disabled" or "active": this is the state applied |
| // next time we boot and cannot be "inactive" |
| int persistedState; |
| if (safeMediaVolumeEnabled && !safeMediaVolumeBypass && !mEnableCsd) { |
| persistedState = SAFE_MEDIA_VOLUME_ACTIVE; |
| // The state can already be "inactive" here if the user has forced it before |
| // the 30 seconds timeout for forced configuration. In this case we don't reset |
| // it to "active". |
| if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) { |
| if (mMusicActiveMs == 0) { |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE; |
| enforceSafeMediaVolume(caller); |
| } else { |
| // We have existing playback time recorded, already confirmed. |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; |
| mLastMusicActiveTimeMs = 0; |
| } |
| } |
| } else { |
| persistedState = SAFE_MEDIA_VOLUME_DISABLED; |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED; |
| } |
| mMcc = mcc; |
| mAudioHandler.sendMessageAtTime( |
| mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE, |
| persistedState, /*arg2=*/0, |
| /*obj=*/null), /*delay=*/0); |
| } |
| } |
| |
| if (mEnableCsd) { |
| initCsd(); |
| } |
| } |
| |
| private int getTimeoutMsForWarning(@AudioManager.CsdWarning int csdWarning) { |
| switch (csdWarning) { |
| case AudioManager.CSD_WARNING_DOSE_REACHED_1X: |
| return CSD_WARNING_TIMEOUT_MS_DOSE_1X; |
| case AudioManager.CSD_WARNING_DOSE_REPEATED_5X: |
| return CSD_WARNING_TIMEOUT_MS_DOSE_5X; |
| case AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE: |
| return CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE; |
| case AudioManager.CSD_WARNING_ACCUMULATION_START: |
| return CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START; |
| } |
| Log.e(TAG, "Invalid CSD warning " + csdWarning, new Exception()); |
| return -1; |
| } |
| |
| @GuardedBy("mSafeMediaVolumeStateLock") |
| private void setSafeMediaVolumeEnabled(boolean on, String caller) { |
| if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) && (mSafeMediaVolumeState |
| != SAFE_MEDIA_VOLUME_DISABLED)) { |
| if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) { |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE; |
| enforceSafeMediaVolume(caller); |
| } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) { |
| mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; |
| mMusicActiveMs = 1; // nonzero = confirmed |
| mLastMusicActiveTimeMs = 0; |
| saveMusicActiveMs(); |
| scheduleMusicActiveCheck(); |
| } |
| } |
| } |
| |
| @GuardedBy("mSafeMediaVolumeStateLock") |
| private void cancelMusicActiveCheck() { |
| if (mMusicActiveIntent != null) { |
| mAlarmManager.cancel(mMusicActiveIntent); |
| mMusicActiveIntent = null; |
| } |
| } |
| |
| @GuardedBy("mSafeMediaVolumeStateLock") |
| private void saveMusicActiveMs() { |
| mAudioHandler.obtainMessage(MSG_PERSIST_MUSIC_ACTIVE_MS, mMusicActiveMs, 0).sendToTarget(); |
| } |
| |
| private int getSafeUsbMediaVolumeIndex() { |
| // determine UI volume index corresponding to the wanted safe gain in dBFS |
| int min = MIN_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]; |
| int max = MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]; |
| |
| mSafeUsbMediaVolumeDbfs = mContext.getResources().getInteger( |
| com.android.internal.R.integer.config_safe_media_volume_usb_mB) / 100.0f; |
| |
| while (Math.abs(max - min) > 1) { |
| int index = (max + min) / 2; |
| float gainDB = AudioSystem.getStreamVolumeDB( |
| AudioSystem.STREAM_MUSIC, index, AudioSystem.DEVICE_OUT_USB_HEADSET); |
| if (Float.isNaN(gainDB)) { |
| //keep last min in case of read error |
| break; |
| } else if (gainDB == mSafeUsbMediaVolumeDbfs) { |
| min = index; |
| break; |
| } else if (gainDB < mSafeUsbMediaVolumeDbfs) { |
| min = index; |
| } else { |
| max = index; |
| } |
| } |
| return min * 10; |
| } |
| |
| private void onPersistSafeVolumeState(int state) { |
| mSettings.putGlobalInt(mAudioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_VOLUME_STATE, |
| state); |
| } |
| |
| private static String safeMediaVolumeStateToString(int state) { |
| switch(state) { |
| case SAFE_MEDIA_VOLUME_NOT_CONFIGURED: return "SAFE_MEDIA_VOLUME_NOT_CONFIGURED"; |
| case SAFE_MEDIA_VOLUME_DISABLED: return "SAFE_MEDIA_VOLUME_DISABLED"; |
| case SAFE_MEDIA_VOLUME_INACTIVE: return "SAFE_MEDIA_VOLUME_INACTIVE"; |
| case SAFE_MEDIA_VOLUME_ACTIVE: return "SAFE_MEDIA_VOLUME_ACTIVE"; |
| } |
| return null; |
| } |
| |
| @GuardedBy("mCsdStateLock") |
| private void updateSoundDoseRecords_l(SoundDoseRecord[] newRecords, float currentCsd) { |
| long totalDuration = 0; |
| for (SoundDoseRecord record : newRecords) { |
| Log.i(TAG, " new record: " + record); |
| totalDuration += record.duration; |
| |
| if (record.value < 0) { |
| // Negative value means the record timestamp exceeded the CSD rolling window size |
| // and needs to be removed |
| if (!mDoseRecords.removeIf( |
| r -> r.value == -record.value && r.timestamp == record.timestamp |
| && r.averageMel == record.averageMel |
| && r.duration == record.duration)) { |
| Log.w(TAG, "Could not find cached record to remove: " + record); |
| } |
| } else { |
| mDoseRecords.add(record); |
| } |
| } |
| |
| mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES, |
| /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0); |
| |
| mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration)); |
| } |
| |
| @SuppressWarnings("GuardedBy") // avoid limitation with intra-procedural analysis of lambdas |
| private void onPersistSoundDoseRecords() { |
| synchronized (mCsdStateLock) { |
| if (mGlobalTimeOffsetInSecs == GLOBAL_TIME_OFFSET_UNINITIALIZED) { |
| mGlobalTimeOffsetInSecs = System.currentTimeMillis() / 1000L; |
| } |
| |
| mSettings.putGlobalString(mAudioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE, |
| Float.toString(mCurrentCsd)); |
| mSettings.putGlobalString(mAudioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING, |
| Float.toString(mNextCsdWarning)); |
| mSettings.putGlobalString(mAudioService.getContentResolver(), |
| Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS, |
| mDoseRecords.stream().map( |
| record -> SoundDoseHelper.recordToPersistedString(record, |
| mGlobalTimeOffsetInSecs)).collect( |
| Collectors.joining(PERSIST_CSD_RECORD_SEPARATOR_CHAR))); |
| } |
| } |
| |
| private static String recordToPersistedString(SoundDoseRecord record, |
| long globalTimeOffsetInSecs) { |
| return convertToGlobalTime(record.timestamp, globalTimeOffsetInSecs) |
| + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.duration |
| + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.value |
| + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.averageMel; |
| } |
| |
| private static long convertToGlobalTime(long bootTimeInSecs, long globalTimeOffsetInSecs) { |
| return bootTimeInSecs + globalTimeOffsetInSecs; |
| } |
| |
| private static long convertToBootTime(long globalTimeInSecs, long globalTimeOffsetInSecs) { |
| return globalTimeInSecs - globalTimeOffsetInSecs; |
| } |
| |
| private static List<SoundDoseRecord> persistedStringToRecordList(String records, |
| long globalTimeOffsetInSecs) { |
| if (records == null || records.isEmpty()) { |
| return null; |
| } |
| return Arrays.stream(TextUtils.split(records, PERSIST_CSD_RECORD_SEPARATOR)).map( |
| record -> SoundDoseHelper.persistedStringToRecord(record, |
| globalTimeOffsetInSecs)).filter(Objects::nonNull).collect( |
| Collectors.toList()); |
| } |
| |
| private static SoundDoseRecord persistedStringToRecord(String record, |
| long globalTimeOffsetInSecs) { |
| if (record == null || record.isEmpty()) { |
| return null; |
| } |
| final String[] fields = TextUtils.split(record, PERSIST_CSD_RECORD_FIELD_SEPARATOR); |
| if (fields.length != 4) { |
| Log.w(TAG, "Expecting 4 fields for a SoundDoseRecord, parsed " + fields.length); |
| return null; |
| } |
| |
| final SoundDoseRecord sdRecord = new SoundDoseRecord(); |
| try { |
| sdRecord.timestamp = convertToBootTime(Long.parseLong(fields[0]), |
| globalTimeOffsetInSecs); |
| sdRecord.duration = Integer.parseInt(fields[1]); |
| sdRecord.value = Float.parseFloat(fields[2]); |
| sdRecord.averageMel = Float.parseFloat(fields[3]); |
| } catch (NumberFormatException e) { |
| Log.e(TAG, "Unable to parse persisted SoundDoseRecord: " + record, e); |
| return null; |
| } |
| |
| return sdRecord; |
| } |
| |
| private float parseGlobalSettingFloat(String audioSafeCsdCurrentValue, float defaultValue) { |
| String stringValue = mSettings.getGlobalString(mAudioService.getContentResolver(), |
| audioSafeCsdCurrentValue); |
| if (stringValue == null || stringValue.isEmpty()) { |
| Log.v(TAG, "No value stored in settings " + audioSafeCsdCurrentValue); |
| return defaultValue; |
| } |
| |
| float value; |
| try { |
| value = Float.parseFloat(stringValue); |
| } catch (NumberFormatException e) { |
| Log.e(TAG, "Error parsing float from settings " + audioSafeCsdCurrentValue, e); |
| value = defaultValue; |
| } |
| |
| return value; |
| } |
| |
| // StreamVolumeCommand contains the information needed to defer the process of |
| // setStreamVolume() in case the user has to acknowledge the safe volume warning message. |
| private static class StreamVolumeCommand { |
| public final int mStreamType; |
| public final int mIndex; |
| public final int mFlags; |
| public final int mDevice; |
| |
| StreamVolumeCommand(int streamType, int index, int flags, int device) { |
| mStreamType = streamType; |
| mIndex = index; |
| mFlags = flags; |
| mDevice = device; |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=") |
| .append(mIndex).append(",flags=").append(mFlags).append(",device=") |
| .append(mDevice).append('}').toString(); |
| } |
| } |
| } |