VolumeZen: SystemUI now hosts the volume dialog.

- Allow SystemUI to set the volume controller interface using
  a new binder call to audio service.
- Remove VolumePanel's dependency on AudioService.
- Host the base VolumePanel in the SystemUI process.

Change-Id: I095d5a1a579d42b68d0f81abb4087bd0c754b876
diff --git a/Android.mk b/Android.mk
index 58509a7..2e78969 100644
--- a/Android.mk
+++ b/Android.mk
@@ -306,6 +306,7 @@
 	media/java/android/media/IRemoteDisplayProvider.aidl \
 	media/java/android/media/IRemoteVolumeObserver.aidl \
 	media/java/android/media/IRingtonePlayer.aidl \
+	media/java/android/media/IVolumeController.aidl \
         media/java/android/media/routeprovider/IRouteConnection.aidl \
         media/java/android/media/routeprovider/IRouteProvider.aidl \
         media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 575667d..9803161 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -37,7 +37,6 @@
 import android.provider.Settings;
 import android.util.Log;
 import android.view.KeyEvent;
-import android.view.VolumePanel;
 
 import java.util.HashMap;
 
@@ -498,7 +497,7 @@
         int keyCode = event.getKeyCode();
         if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP
                 && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
-                && mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY
+                && mVolumeKeyUpTime + AudioService.PLAY_SOUND_DELAY
                         > SystemClock.uptimeMillis()) {
             /*
              * The user has hit another key during the delay (e.g., 300ms)
@@ -2892,4 +2891,79 @@
         return AudioSystem.getOutputLatency(streamType);
     }
 
+    /**
+     * Registers a global volume controller interface.  Currently limited to SystemUI.
+     *
+     * @hide
+     */
+    public void setVolumeController(IVolumeController controller) {
+        try {
+            getService().setVolumeController(controller);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error setting volume controller", e);
+        }
+    }
+
+    /**
+     * Only useful for volume controllers.
+     * @hide
+     */
+    public int getRemoteStreamVolume() {
+        try {
+            return getService().getRemoteStreamVolume();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error getting remote stream volume", e);
+            return 0;
+        }
+    }
+
+    /**
+     * Only useful for volume controllers.
+     * @hide
+     */
+    public int getRemoteStreamMaxVolume() {
+        try {
+            return getService().getRemoteStreamMaxVolume();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error getting remote stream max volume", e);
+            return 0;
+        }
+    }
+
+    /**
+     * Only useful for volume controllers.
+     * @hide
+     */
+    public void setRemoteStreamVolume(int index) {
+        try {
+            getService().setRemoteStreamVolume(index);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error setting remote stream volume", e);
+        }
+    }
+
+    /**
+     * Only useful for volume controllers.
+     * @hide
+     */
+    public boolean isStreamAffectedByRingerMode(int streamType) {
+        try {
+            return getService().isStreamAffectedByRingerMode(streamType);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling isStreamAffectedByRingerMode", e);
+            return false;
+        }
+    }
+
+    /**
+     * Only useful for volume controllers.
+     * @hide
+     */
+    public void disableSafeMediaVolume() {
+        try {
+            getService().disableSafeMediaVolume();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error disabling safe media volume", e);
+        }
+    }
 }
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 6e623a5..c736fc7 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -67,7 +67,6 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.Surface;
-import android.view.VolumePanel;
 import android.view.WindowManager;
 
 import com.android.internal.telephony.ITelephony;
@@ -116,13 +115,21 @@
     /** How long to delay before persisting a change in volume/ringer mode. */
     private static final int PERSIST_DELAY = 500;
 
+    /**
+     * The delay before playing a sound. This small period exists so the user
+     * can press another key (non-volume keys, too) to have it NOT be audible.
+     * <p>
+     * PhoneWindow will implement this part.
+     */
+    public static final int PLAY_SOUND_DELAY = 300;
+
     private final Context mContext;
     private final ContentResolver mContentResolver;
     private final AppOpsManager mAppOps;
     private final boolean mVoiceCapable;
 
-    /** The UI */
-    private VolumePanel mVolumePanel;
+    /** The controller for the volume UI. */
+    private final VolumeController mVolumeController = new VolumeController();
 
     // sendMsg() flags
     /** If the msg is already queued, replace it with this one. */
@@ -477,13 +484,12 @@
         sSoundEffectVolumeDb = context.getResources().getInteger(
                 com.android.internal.R.integer.config_soundEffectVolumeDb);
 
-        mVolumePanel = new VolumePanel(context, this);
         mForcedUseForComm = AudioSystem.FORCE_NONE;
 
         createAudioSystemThread();
 
         mMediaFocusControl = new MediaFocusControl(mAudioHandler.getLooper(),
-                mContext, /*VolumeController*/ mVolumePanel, this);
+                mContext, mVolumeController, this);
 
         AudioSystem.setErrorCallback(mAudioSystemCallback);
 
@@ -953,7 +959,7 @@
             if ((direction == AudioManager.ADJUST_RAISE) &&
                     !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
                 Log.e(TAG, "adjustStreamVolume() safe volume index = "+oldIndex);
-                mVolumePanel.postDisplaySafeVolumeWarning(flags);
+                mVolumeController.postDisplaySafeVolumeWarning(flags);
             } else if (streamState.adjustIndex(direction * step, device)) {
                 // Post message to set system volume (it in turn will post a message
                 // to persist). Do not change volume if stream is muted.
@@ -1081,7 +1087,7 @@
             }
 
             if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
-                mVolumePanel.postDisplaySafeVolumeWarning(flags);
+                mVolumeController.postDisplaySafeVolumeWarning(flags);
                 mPendingVolumeCommand = new StreamVolumeCommand(
                                                     streamType, index, flags, device);
             } else {
@@ -1202,7 +1208,7 @@
             streamType = AudioSystem.STREAM_NOTIFICATION;
         }
 
-        mVolumePanel.postVolumeChanged(streamType, flags);
+        mVolumeController.postVolumeChanged(streamType, flags);
 
         if ((flags & AudioManager.FLAG_FIXED_VOLUME) == 0) {
             oldIndex = (oldIndex + 5) / 10;
@@ -1217,7 +1223,7 @@
 
     // UI update and Broadcast Intent
     private void sendMasterVolumeUpdate(int flags, int oldVolume, int newVolume) {
-        mVolumePanel.postMasterVolumeChanged(flags);
+        mVolumeController.postMasterVolumeChanged(flags);
 
         Intent intent = new Intent(AudioManager.MASTER_VOLUME_CHANGED_ACTION);
         intent.putExtra(AudioManager.EXTRA_PREV_MASTER_VOLUME_VALUE, oldVolume);
@@ -1227,7 +1233,7 @@
 
     // UI update and Broadcast Intent
     private void sendMasterMuteUpdate(boolean muted, int flags) {
-        mVolumePanel.postMasterMuteChanged(flags);
+        mVolumeController.postMasterMuteChanged(flags);
         broadcastMasterMuteStatus(muted);
     }
 
@@ -2589,6 +2595,7 @@
         return adjustVolumeIndex;
     }
 
+    @Override
     public boolean isStreamAffectedByRingerMode(int streamType) {
         return (mRingerModeAffectedStreams & (1 << streamType)) != 0;
     }
@@ -4309,15 +4316,19 @@
         mMediaFocusControl.registerRemoteVolumeObserverForRcc(rccId, rvo);
     }
 
+    @Override
     public int getRemoteStreamVolume() {
         return mMediaFocusControl.getRemoteStreamVolume();
     }
 
+    @Override
     public int getRemoteStreamMaxVolume() {
         return mMediaFocusControl.getRemoteStreamMaxVolume();
     }
 
+    @Override
     public void setRemoteStreamVolume(int index) {
+        enforceSelfOrSystemUI("set the remote stream volume");
         mMediaFocusControl.setRemoteStreamVolume(index);
     }
 
@@ -4450,7 +4461,7 @@
                     }
                 }
             }
-            mVolumePanel.setLayoutDirection(config.getLayoutDirection());
+            mVolumeController.setLayoutDirection(config.getLayoutDirection());
         } catch (Exception e) {
             Log.e(TAG, "Error handling configuration change: ", e);
         }
@@ -4625,7 +4636,9 @@
         }
     }
 
+    @Override
     public void disableSafeMediaVolume() {
+        enforceSelfOrSystemUI("disable the safe media volume");
         synchronized (mSafeMediaVolumeState) {
             setSafeMediaVolumeEnabled(false);
             if (mPendingVolumeCommand != null) {
@@ -4681,6 +4694,7 @@
         pw.println("\nAudio routes:");
         pw.print("  mMainType=0x"); pw.println(Integer.toHexString(mCurAudioRoutes.mMainType));
         pw.print("  mBluetoothName="); pw.println(mCurAudioRoutes.mBluetoothName);
+        pw.print("  mVolumeController="); pw.println(mVolumeController);
     }
 
     // Inform AudioFlinger of our device's low RAM attribute
@@ -4691,4 +4705,39 @@
             Log.w(TAG, "AudioFlinger informed of device's low RAM attribute; status " + status);
         }
     }
+
+    private void enforceSelfOrSystemUI(String action) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR_SERVICE,
+                "Only SystemUI can " + action);
+    }
+
+    @Override
+    public void setVolumeController(final IVolumeController controller) {
+        enforceSelfOrSystemUI("set the volume controller");
+
+        // return early if things are not actually changing
+        if (mVolumeController.isSameBinder(controller)) {
+            return;
+        }
+
+        // dismiss the old volume controller
+        mVolumeController.postDismiss();
+        if (controller != null) {
+            // we are about to register a new controller, listen for its death
+            try {
+                controller.asBinder().linkToDeath(new DeathRecipient() {
+                    @Override
+                    public void binderDied() {
+                        if (mVolumeController.isSameBinder(controller)) {
+                            Log.w(TAG, "Current remote volume controller died, unregistering");
+                            setVolumeController(null);
+                        }
+                    }
+                }, 0);
+            } catch (RemoteException e) {
+                // noop
+            }
+        }
+        mVolumeController.setController(controller);
+    }
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 2f08325..30de4f9 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -26,6 +26,7 @@
 import android.media.IRemoteControlDisplay;
 import android.media.IRemoteVolumeObserver;
 import android.media.IRingtonePlayer;
+import android.media.IVolumeController;
 import android.media.Rating;
 import android.net.Uri;
 import android.view.KeyEvent;
@@ -236,4 +237,10 @@
     AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer);
 
     boolean isCameraSoundForced();
+
+    void setVolumeController(in IVolumeController controller);
+
+    boolean isStreamAffectedByRingerMode(int streamType);
+
+    void disableSafeMediaVolume();
 }
diff --git a/media/java/android/media/IVolumeController.aidl b/media/java/android/media/IVolumeController.aidl
new file mode 100644
index 0000000..35d7708
--- /dev/null
+++ b/media/java/android/media/IVolumeController.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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 android.media;
+
+
+/**
+ * AIDL for the AudioService to report interesting events to a remote volume control dialog.
+ * @hide
+ */
+oneway interface IVolumeController {
+    void hasNewRemotePlaybackInfo();
+
+    void remoteVolumeChanged(int streamType, int flags);
+
+    void remoteSliderVisibility(boolean visible);
+
+    void displaySafeVolumeWarning(int flags);
+
+    void volumeChanged(int streamType, int flags);
+
+    void masterVolumeChanged(int flags);
+
+    void masterMuteChanged(int flags);
+
+    void setLayoutDirection(int layoutDirection);
+
+    void dismiss();
+}
diff --git a/media/java/android/media/VolumeController.java b/media/java/android/media/VolumeController.java
index 2d12bf2..6b70cc3 100644
--- a/media/java/android/media/VolumeController.java
+++ b/media/java/android/media/VolumeController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2014 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.
@@ -16,14 +16,120 @@
 
 package android.media;
 
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Objects;
+
 /**
+ * Wraps the remote volume controller interface as a convenience to audio service.
  * @hide
  */
-public interface VolumeController {
+public class VolumeController {
+    private static final String TAG = "VolumeController";
 
-    public void postHasNewRemotePlaybackInfo();
+    private IVolumeController mController;
 
-    public void postRemoteVolumeChanged(int streamType, int flags);
+    public void setController(IVolumeController controller) {
+        mController = controller;
+    }
 
-    public void postRemoteSliderVisibility(boolean visible);
-}
+    public boolean isSameBinder(IVolumeController controller) {
+        return Objects.equals(asBinder(), binder(controller));
+    }
+
+    public IBinder asBinder() {
+        return binder(mController);
+    }
+
+    private static IBinder binder(IVolumeController controller) {
+        return controller == null ? null : controller.asBinder();
+    }
+
+    @Override
+    public String toString() {
+        return "VolumeController(" + asBinder() + ")";
+    }
+
+    public void postHasNewRemotePlaybackInfo() {
+        if (mController == null) return;
+        try {
+            mController.hasNewRemotePlaybackInfo();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling hasNewRemotePlaybackInfo", e);
+        }
+    }
+
+    public void postRemoteVolumeChanged(int streamType, int flags) {
+        if (mController == null) return;
+        try {
+            mController.remoteVolumeChanged(streamType, flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling remoteVolumeChanged", e);
+        }
+    }
+
+    public void postRemoteSliderVisibility(boolean visible) {
+        if (mController == null) return;
+        try {
+            mController.remoteSliderVisibility(visible);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling remoteSliderVisibility", e);
+        }
+    }
+
+    public void postDisplaySafeVolumeWarning(int flags) {
+        if (mController == null) return;
+        try {
+            mController.displaySafeVolumeWarning(flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling displaySafeVolumeWarning", e);
+        }
+    }
+
+    public void postVolumeChanged(int streamType, int flags) {
+        if (mController == null) return;
+        try {
+            mController.volumeChanged(streamType, flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling volumeChanged", e);
+        }
+    }
+
+    public void postMasterVolumeChanged(int flags) {
+        if (mController == null) return;
+        try {
+            mController.masterVolumeChanged(flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling masterVolumeChanged", e);
+        }
+    }
+
+    public void postMasterMuteChanged(int flags) {
+        if (mController == null) return;
+        try {
+            mController.masterMuteChanged(flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling masterMuteChanged", e);
+        }
+    }
+
+    public void setLayoutDirection(int layoutDirection) {
+        if (mController == null) return;
+        try {
+            mController.setLayoutDirection(layoutDirection);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling setLayoutDirection", e);
+        }
+    }
+
+    public void postDismiss() {
+        if (mController == null) return;
+        try {
+            mController.dismiss();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Error calling dismiss", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 217074f..d7ce255 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -47,6 +47,7 @@
             com.android.systemui.power.PowerUI.class,
             com.android.systemui.media.RingtonePlayer.class,
             com.android.systemui.settings.SettingsUI.class,
+            com.android.systemui.volume.VolumeUI.class,
     };
 
     /**
diff --git a/core/java/android/view/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
similarity index 96%
rename from core/java/android/view/VolumePanel.java
rename to packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
index 4730e59..8657e07 100644
--- a/core/java/android/view/VolumePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.view;
+package com.android.systemui.volume;
 
 import com.android.internal.R;
 
@@ -32,12 +32,18 @@
 import android.media.AudioSystem;
 import android.media.RingtoneManager;
 import android.media.ToneGenerator;
-import android.media.VolumeController;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Message;
 import android.os.Vibrator;
 import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 import android.widget.ImageView;
 import android.widget.SeekBar;
@@ -46,27 +52,15 @@
 import java.util.HashMap;
 
 /**
- * Handle the volume up and down keys.
- *
- * This code really should be moved elsewhere.
- *
- * Seriously, it really really should be moved elsewhere.  This is used by
- * android.media.AudioService, which actually runs in the system process, to
- * show the volume dialog when the user changes the volume.  What a mess.
+ * Handles the user interface for the volume keys.
  *
  * @hide
  */
-public class VolumePanel extends Handler implements VolumeController {
+public class VolumePanel extends Handler {
     private static final String TAG = VolumePanel.class.getSimpleName();
     private static boolean LOGD = false;
 
-    /**
-     * The delay before playing a sound. This small period exists so the user
-     * can press another key (non-volume keys, too) to have it NOT be audible.
-     * <p>
-     * PhoneWindow will implement this part.
-     */
-    public static final int PLAY_SOUND_DELAY = 300;
+    private static final int PLAY_SOUND_DELAY = AudioService.PLAY_SOUND_DELAY;
 
     /**
      * The delay before vibrating. This small period exists so if the user is
@@ -99,9 +93,8 @@
     private static final int STREAM_MASTER = -100;
     // Pseudo stream type for remote volume is defined in AudioService.STREAM_REMOTE_MUSIC
 
-    protected Context mContext;
-    private AudioManager mAudioManager;
-    protected AudioService mAudioService;
+    protected final Context mContext;
+    private final AudioManager mAudioManager;
     private boolean mRingIsSilent;
     private boolean mShowCombinedVolumes;
     private boolean mVoiceCapable;
@@ -252,10 +245,9 @@
     }
 
 
-    public VolumePanel(Context context, AudioService volumeService) {
+    public VolumePanel(Context context) {
         mContext = context;
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-        mAudioService = volumeService;
 
         // For now, only show master volume if master volume is supported
         final Resources res = context.getResources();
@@ -367,7 +359,7 @@
         if (streamType == STREAM_MASTER) {
             return mAudioManager.isMasterMute();
         } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
-            return (mAudioService.getRemoteStreamVolume() <= 0);
+            return (mAudioManager.getRemoteStreamVolume() <= 0);
         } else {
             return mAudioManager.isStreamMute(streamType);
         }
@@ -377,7 +369,7 @@
         if (streamType == STREAM_MASTER) {
             return mAudioManager.getMasterMaxVolume();
         } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
-            return mAudioService.getRemoteStreamMaxVolume();
+            return mAudioManager.getRemoteStreamMaxVolume();
         } else {
             return mAudioManager.getStreamMaxVolume(streamType);
         }
@@ -387,7 +379,7 @@
         if (streamType == STREAM_MASTER) {
             return mAudioManager.getMasterVolume();
         } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
-            return mAudioService.getRemoteStreamVolume();
+            return mAudioManager.getRemoteStreamVolume();
         } else {
             return mAudioManager.getStreamVolume(streamType);
         }
@@ -397,7 +389,7 @@
         if (streamType == STREAM_MASTER) {
             mAudioManager.setMasterVolume(index, flags);
         } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) {
-            mAudioService.setRemoteStreamVolume(index);
+            mAudioManager.setRemoteStreamVolume(index);
         } else {
             mAudioManager.setStreamVolume(streamType, index, flags);
         }
@@ -531,7 +523,6 @@
         obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget();
     }
 
-    @Override
     public void postRemoteVolumeChanged(int streamType, int flags) {
         if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return;
         synchronized (this) {
@@ -543,7 +534,6 @@
         obtainMessage(MSG_REMOTE_VOLUME_CHANGED, streamType, flags).sendToTarget();
     }
 
-    @Override
     public void postRemoteSliderVisibility(boolean visible) {
         obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED,
                 AudioService.STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget();
@@ -560,7 +550,6 @@
      * as a request to update the volume), the application will likely set a new volume. If the UI
      * is still up, we need to refresh the display to show this new value.
      */
-    @Override
     public void postHasNewRemotePlaybackInfo() {
         if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return;
         // don't create or prevent resources to be freed, if they disappear, this update came too
@@ -592,6 +581,11 @@
         obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget();
     }
 
+    public void postDismiss() {
+        removeMessages(MSG_TIMEOUT);
+        sendEmptyMessage(MSG_TIMEOUT);
+    }
+
     /**
      * 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
@@ -751,7 +745,7 @@
         // Do a little vibrate if applicable (only when going into vibrate mode)
         if ((streamType != AudioService.STREAM_REMOTE_MUSIC) &&
                 ((flags & AudioManager.FLAG_VIBRATE) != 0) &&
-                mAudioService.isStreamAffectedByRingerMode(streamType) &&
+                mAudioManager.isStreamAffectedByRingerMode(streamType) &&
                 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) {
             sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY);
         }
@@ -874,7 +868,7 @@
                                             new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
-                                mAudioService.disableSafeMediaVolume();
+                                mAudioManager.disableSafeMediaVolume();
                             }
                         })
                         .setNegativeButton(com.android.internal.R.string.no, null)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
new file mode 100644
index 0000000..9bd75b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -0,0 +1,125 @@
+package com.android.systemui.volume;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.IVolumeController;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.systemui.SystemUI;
+
+/*
+ * Copyright (C) 2014 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.
+ */
+
+public class VolumeUI extends SystemUI {
+    private static final String TAG = "VolumeUI";
+    private static final String SETTING = "systemui_volume_controller";  // for testing
+    private static final Uri SETTING_URI = Settings.Global.getUriFor(SETTING);
+    private static final int DEFAULT = 1;  // enabled by default
+
+    private AudioManager mAudioManager;
+    private VolumeController mVolumeController;
+
+    @Override
+    public void start() {
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        updateController();
+        mContext.getContentResolver().registerContentObserver(SETTING_URI, false, mObserver);
+    }
+
+    private void updateController() {
+        if (Settings.Global.getInt(mContext.getContentResolver(), SETTING, DEFAULT) != 0) {
+            if (mVolumeController == null) {
+                mVolumeController = new VolumeController(mContext);
+            }
+            Log.d(TAG, "Registering volume controller");
+            mAudioManager.setVolumeController(mVolumeController);
+        } else {
+            Log.d(TAG, "Unregistering volume controller");
+            mAudioManager.setVolumeController(null);
+        }
+    }
+
+    private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+        public void onChange(boolean selfChange, Uri uri) {
+            if (SETTING_URI.equals(uri)) {
+                updateController();
+            }
+        }
+    };
+
+    /** For now, simply host an unmodified base volume panel in this process. */
+    private final class VolumeController extends IVolumeController.Stub {
+        private final VolumePanel mPanel;
+
+        public VolumeController(Context context) {
+            mPanel = new VolumePanel(context);
+        }
+
+        @Override
+        public void hasNewRemotePlaybackInfo() throws RemoteException {
+            mPanel.postHasNewRemotePlaybackInfo();
+        }
+
+        @Override
+        public void remoteVolumeChanged(int streamType, int flags)
+                throws RemoteException {
+            mPanel.postRemoteVolumeChanged(streamType, flags);
+        }
+
+        @Override
+        public void remoteSliderVisibility(boolean visible)
+                throws RemoteException {
+            mPanel.postRemoteSliderVisibility(visible);
+        }
+
+        @Override
+        public void displaySafeVolumeWarning(int flags) throws RemoteException {
+            mPanel.postDisplaySafeVolumeWarning(flags);
+        }
+
+        @Override
+        public void volumeChanged(int streamType, int flags)
+                throws RemoteException {
+            mPanel.postVolumeChanged(streamType, flags);
+        }
+
+        @Override
+        public void masterVolumeChanged(int flags) throws RemoteException {
+            mPanel.postMasterVolumeChanged(flags);
+        }
+
+        @Override
+        public void masterMuteChanged(int flags) throws RemoteException {
+            mPanel.postMasterMuteChanged(flags);
+        }
+
+        @Override
+        public void setLayoutDirection(int layoutDirection)
+                throws RemoteException {
+            mPanel.setLayoutDirection(layoutDirection);
+        }
+
+        @Override
+        public void dismiss() throws RemoteException {
+            mPanel.postDismiss();
+        }
+    }
+}