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
diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java
index 4d4e985c..c93da06 100644
--- a/core/java/android/view/VolumePanel.java
+++ b/core/java/android/view/VolumePanel.java
@@ -18,6 +18,7 @@
 
 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 @@
     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 @@
     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 @@
         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 @@
         }
     }
 
+    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 @@
             case MSG_SLIDER_VISIBILITY_CHANGED:
                 onSliderVisibilityChanged(msg.arg1, msg.arg2);
                 break;
+
+            case MSG_DISPLAY_SAFE_VOLUME_WARNING:
+                onDisplaySafeVolumeWarning();
+                break;
         }
     }
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index c97c214..9c65f86 100755
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -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>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 1950d84..c532888 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -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>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d4e82b5..d4bf7ed 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -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" />
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index e5c2a4d..4459d03 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -153,6 +153,7 @@
     // 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 @@
         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 @@
         updateStreamVolumeAlias(false /*updateVolumes*/);
         createStreamStates();
 
+        synchronized (mSafeMediaVolumeEnabled) {
+            enforceSafeMediaVolume();
+        }
+
         mMediaServerOk = true;
 
         // Call setRingerModeInt() to apply correct mute
@@ -738,6 +745,11 @@
         // 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 @@
         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 @@
 
         checkAllAliasStreamVolumes();
 
+        synchronized (mSafeMediaVolumeEnabled) {
+            enforceSafeMediaVolume();
+        }
+
         // apply new ringer mode
         setRingerModeInt(getRingerMode(), false);
     }
@@ -2138,6 +2159,33 @@
                 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 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 @@
                 case MSG_SET_RSX_CONNECTION_STATE:
                     onSetRsxConnectionState(msg.arg1/*available*/, msg.arg2/*address*/);
                     break;
+
+                case MSG_CHECK_MUSIC_ACTIVE:
+                    onCheckMusicActive();
+                    break;
             }
         }
     }
@@ -4426,7 +4486,7 @@
                         "  -- vol: " + rcse.mPlaybackVolume +
                         "  -- volMax: " + rcse.mPlaybackVolumeMax +
                         "  -- volObs: " + rcse.mRemoteVolumeObs);
-                
+
             }
         }
         synchronized (mMainRemote) {
@@ -5415,6 +5475,109 @@
         }
     }
 
+
+    //==========================================================================================
+    // 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);