Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c34dcc1e authored by Eric Laurent's avatar Eric Laurent
Browse files

headphone volume limitation

Limit music volume when headphones or headset are inserted.
Display warning message when user wants to increase the volume
above a platform specific volume and request user acknowledgement
before proceeding.

TODO: exact wording of the warning message must be defined by UX.

Change-Id: I00f429f602534c6d8783126b929371c4d432e6e2
parent b45965f5
Loading
Loading
Loading
Loading
+61 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.view;

import com.android.internal.R;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface.OnDismissListener;
import android.content.BroadcastReceiver;
@@ -92,6 +93,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
    private static final int MSG_REMOTE_VOLUME_CHANGED = 8;
    private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9;
    private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10;
    private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11;

    // Pseudo stream type for master volume
    private static final int STREAM_MASTER = -100;
@@ -211,6 +213,31 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
    private ToneGenerator mToneGenerators[];
    private Vibrator mVibrator;

    private static AlertDialog sConfirmSafeVolumeDialog;

    private static class WarningDialogReceiver extends BroadcastReceiver
            implements DialogInterface.OnDismissListener {
        private Context mContext;
        private Dialog mDialog;

        WarningDialogReceiver(Context context, Dialog dialog) {
            mContext = context;
            mDialog = dialog;
            IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
            context.registerReceiver(this, filter);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            mDialog.cancel();
        }

        public void onDismiss(DialogInterface unused) {
            mContext.unregisterReceiver(this);
        }
    }


    public VolumePanel(final Context context, AudioService volumeService) {
        mContext = context;
        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -528,6 +555,10 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
        postMuteChanged(STREAM_MASTER, flags);
    }

    public void postDisplaySafeVolumeWarning() {
        obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, 0, 0).sendToTarget();
    }

    /**
     * Override this if you have other work to do when the volume changes (for
     * example, vibrating, playing a sound, etc.). Make sure to call through to
@@ -796,6 +827,32 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
        }
    }

    protected void onDisplaySafeVolumeWarning() {
        if (sConfirmSafeVolumeDialog != null) {
            sConfirmSafeVolumeDialog.dismiss();
        }
        sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext)
                .setTitle(android.R.string.dialog_alert_title)
                .setMessage(com.android.internal.R.string.safe_media_volume_warning)
                .setPositiveButton(com.android.internal.R.string.yes,
                                    new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        mAudioService.disableSafeMediaVolume();
                    }
                })
                .setNegativeButton(com.android.internal.R.string.no, null)
                .setIconAttribute(android.R.attr.alertDialogIcon)
                .create();

        final WarningDialogReceiver warning = new WarningDialogReceiver(mContext,
                sConfirmSafeVolumeDialog);

        sConfirmSafeVolumeDialog.setOnDismissListener(warning);
        sConfirmSafeVolumeDialog.getWindow().setType(
                                                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
        sConfirmSafeVolumeDialog.show();
    }

    /**
     * Lock on this VolumePanel instance as long as you use the returned ToneGenerator.
     */
@@ -910,6 +967,10 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
            case MSG_SLIDER_VISIBILITY_CHANGED:
                onSliderVisibilityChanged(msg.arg1, msg.arg2);
                break;

            case MSG_DISPLAY_SAFE_VOLUME_WARNING:
                onDisplaySafeVolumeWarning();
                break;
        }
    }

+6 −0
Original line number Diff line number Diff line
@@ -931,4 +931,10 @@
         Devices with screens that deviate too far from their assigned density
         bucket should consider tuning this value in a device-specific overlay. -->
    <dimen name="config_minScalingSpan">25mm</dimen>

    <!-- Safe headphone volume index. When music stream volume is below this index
    the SPL on headphone output is compliant to EN 60950 requirements for portable music
    players. -->
    <integer name="config_safe_media_volume_index">10</integer>

</resources>
+6 −0
Original line number Diff line number Diff line
@@ -3880,6 +3880,12 @@
       Try again in <xliff:g id="number">%d</xliff:g> seconds.
    </string>

    <!-- Message shown in dialog when user is attempting to set the music volume above the
    recommended maximum level for headphones -->
    <string name="safe_media_volume_warning" product="default">
       "Raise volume above the recommended level?"
    </string>

    <string name="kg_temp_back_string"> &lt; </string> <!-- TODO: remove this -->

</resources>
+2 −0
Original line number Diff line number Diff line
@@ -279,6 +279,7 @@
  <java-symbol type="integer" name="config_soundEffectVolumeDb" />
  <java-symbol type="integer" name="config_lockSoundVolumeDb" />
  <java-symbol type="integer" name="config_multiuserMaximumUsers" />
  <java-symbol type="integer" name="config_safe_media_volume_index" />

  <java-symbol type="color" name="tab_indicator_text_v4" />

@@ -810,6 +811,7 @@
  <java-symbol type="string" name="default_audio_route_name_dock_speakers" />
  <java-symbol type="string" name="default_audio_route_name_hdmi" />
  <java-symbol type="string" name="default_audio_route_category_name" />
  <java-symbol type="string" name="safe_media_volume_warning" />

  <java-symbol type="plurals" name="abbrev_in_num_days" />
  <java-symbol type="plurals" name="abbrev_in_num_hours" />
+164 −1
Original line number Diff line number Diff line
@@ -153,6 +153,7 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
    // end of messages handled under wakelock
    private static final int MSG_SET_RSX_CONNECTION_STATE = 23; // change remote submix connection
    private static final int MSG_SET_FORCE_RSX_USE = 24;        // force remote submix audio routing
    private static final int MSG_CHECK_MUSIC_ACTIVE = 25;

    // flags for MSG_PERSIST_VOLUME indicating if current and/or last audible volume should be
    // persisted
@@ -430,6 +431,8 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
        mContentResolver = context.getContentResolver();
        mVoiceCapable = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_voice_capable);
        mSafeMediaVolumeIndex = mContext.getResources().getInteger(
                com.android.internal.R.integer.config_safe_media_volume_index) * 10;

        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
        mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
@@ -454,6 +457,10 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
        updateStreamVolumeAlias(false /*updateVolumes*/);
        createStreamStates();

        synchronized (mSafeMediaVolumeEnabled) {
            enforceSafeMediaVolume();
        }

        mMediaServerOk = true;

        // Call setRingerModeInt() to apply correct mute
@@ -738,6 +745,11 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
        // convert one UI step (+/-1) into a number of internal units on the stream alias
        int step = rescaleIndex(10, streamType, streamTypeAlias);

        if ((direction == AudioManager.ADJUST_RAISE) &&
                !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
            return;
        }

        // If either the client forces allowing ringer modes for this adjustment,
        // or the stream type is one that is affected by ringer modes
        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
@@ -815,12 +827,17 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
        VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];

        final int device = getDeviceForStream(streamType);

        // get last audible index if stream is muted, current index otherwise
        final int oldIndex = streamState.getIndex(device,
                                                  (streamState.muteCount() != 0) /* lastAudible */);

        index = rescaleIndex(index * 10, streamType, mStreamVolumeAlias[streamType]);

        if (!checkSafeMediaVolume(mStreamVolumeAlias[streamType], index, device)) {
            return;
        }

        // setting volume on master stream type also controls silent mode
        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
                (mStreamVolumeAlias[streamType] == getMasterStreamType())) {
@@ -1681,6 +1698,10 @@ public class AudioService extends IAudioService.Stub implements OnFinished {

        checkAllAliasStreamVolumes();

        synchronized (mSafeMediaVolumeEnabled) {
            enforceSafeMediaVolume();
        }

        // apply new ringer mode
        setRingerModeInt(getRingerMode(), false);
    }
@@ -2138,6 +2159,33 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
                String.valueOf(address) /*device_address*/);
    }

    private void onCheckMusicActive() {
        synchronized (mSafeMediaVolumeEnabled) {
            if (!mSafeMediaVolumeEnabled) {
                int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);

                if ((device & mSafeMediaVolumeDevices) != 0) {
                    sendMsg(mAudioHandler,
                            MSG_CHECK_MUSIC_ACTIVE,
                            SENDMSG_REPLACE,
                            device,
                            0,
                            null,
                            MUSIC_ACTIVE_POLL_PERIOD_MS);
                    if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
                        // Approximate cumulative active music time
                        mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
                        if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
                            setSafeMediaVolumeEnabled(true);
                            mMusicActiveMs = 0;
                            mVolumePanel.postDisplaySafeVolumeWarning();
                        }
                    }
                }
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // Internal methods
    ///////////////////////////////////////////////////////////////////////////
@@ -2397,6 +2445,14 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
    public void setWiredDeviceConnectionState(int device, int state, String name) {
        synchronized (mConnectedDevices) {
            int delay = checkSendBecomingNoisyIntent(device, state);
            if ((device & mSafeMediaVolumeDevices) != 0) {
                setSafeMediaVolumeEnabled(state != 0);
                // insert delay to allow new volume to apply before switching to headphones
                if ((delay < SAFE_VOLUME_DELAY_MS) && (state != 0)) {
                    delay = SAFE_VOLUME_DELAY_MS;
                }
            }

            queueMsgUnderWakeLock(mAudioHandler,
                    MSG_SET_WIRED_DEVICE_CONNECTION_STATE,
                    device,
@@ -3168,6 +3224,10 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
                case MSG_SET_RSX_CONNECTION_STATE:
                    onSetRsxConnectionState(msg.arg1/*available*/, msg.arg2/*address*/);
                    break;

                case MSG_CHECK_MUSIC_ACTIVE:
                    onCheckMusicActive();
                    break;
            }
        }
    }
@@ -5415,6 +5475,109 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
        }
    }


    //==========================================================================================
    // Safe media volume management.
    // MUSIC stream volume level is limited when headphones are connected according to safety
    // regulation. When the user attempts to raise the volume above the limit, a warning is
    // displayed and the user has to acknowlegde before the volume is actually changed.
    // The volume index corresponding to the limit is stored in config_safe_media_volume_index
    // property. Platforms with a different limit must set this property accordingly in their
    // overlay.
    //==========================================================================================

    // mSafeMediaVolumeEnabled indicates whether the media volume is limited over headphones.
    // It is true by default when headphones or a headset are inserted and can be overriden by
    // calling AudioService.disableSafeMediaVolume() (when user opts out).
    private Boolean mSafeMediaVolumeEnabled = new Boolean(false);
    // mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
    private final int mSafeMediaVolumeIndex;
    // mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
    private final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET |
                                                AudioSystem.DEVICE_OUT_WIRED_HEADPHONE;
    // 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 static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
    private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
    private static final int SAFE_VOLUME_DELAY_MS = 500;  // 500ms before switching to headphones

    private void setSafeMediaVolumeEnabled(boolean on) {
        synchronized (mSafeMediaVolumeEnabled) {
            if (on && !mSafeMediaVolumeEnabled) {
                enforceSafeMediaVolume();
            } else if (!on && mSafeMediaVolumeEnabled) {
                mMusicActiveMs = 0;
                sendMsg(mAudioHandler,
                        MSG_CHECK_MUSIC_ACTIVE,
                        SENDMSG_REPLACE,
                        0,
                        0,
                        null,
                        MUSIC_ACTIVE_POLL_PERIOD_MS);
            }
            mSafeMediaVolumeEnabled = on;
        }
    }

    private void enforceSafeMediaVolume() {
        VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
        boolean lastAudible = (streamState.muteCount() != 0);
        int devices = mSafeMediaVolumeDevices;
        int i = 0;

        while (devices != 0) {
            int device = 1 << i++;
            if ((device & devices) == 0) {
                continue;
            }
            int index = streamState.getIndex(device, lastAudible);
            if (index > mSafeMediaVolumeIndex) {
                if (lastAudible) {
                    streamState.setLastAudibleIndex(mSafeMediaVolumeIndex, device);
                    sendMsg(mAudioHandler,
                            MSG_PERSIST_VOLUME,
                            SENDMSG_QUEUE,
                            PERSIST_LAST_AUDIBLE,
                            device,
                            streamState,
                            PERSIST_DELAY);
                } else {
                    streamState.setIndex(mSafeMediaVolumeIndex, device, true);
                    sendMsg(mAudioHandler,
                            MSG_SET_DEVICE_VOLUME,
                            SENDMSG_QUEUE,
                            device,
                            0,
                            streamState,
                            0);
                }
            }
            devices &= ~device;
        }
    }

    private boolean checkSafeMediaVolume(int streamType, int index, int device) {
        synchronized (mSafeMediaVolumeEnabled) {
            if (mSafeMediaVolumeEnabled &&
                    (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
                    ((device & mSafeMediaVolumeDevices) != 0) &&
                    (index > mSafeMediaVolumeIndex)) {
                mVolumePanel.postDisplaySafeVolumeWarning();
                return false;
            }
            return true;
        }
    }

    public void disableSafeMediaVolume() {
        synchronized (mSafeMediaVolumeEnabled) {
            setSafeMediaVolumeEnabled(false);
        }
    }


    @Override
    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);