Introduce new volume dialog.

 - New VolumeDialog (presentation) + VolumeDialogController (state)
   to implement a volume dialog that keeps track of multiple audio
   streams, including all remote streams.
 - The dialog starts out with a single stream, with more detail available
   behind an expand chevron.
 - Existing zen options reorganized under a master switch bar
   named "Block interruptions", with "None" renamed to "No interruptions"
   and "Priority" renamed to "Priority only".
 - Combined "Block interruptions" icon replaces the now-obsolete star/no-smoking
   icons in the status bar.
 - New icons for all sliders.
 - All sliders present a continuous facade, mapped to discrete integer units
   under the hood.
 - All interesting volume events and state changes piped through one central
   helper for future routing.
 - VolumePanel is obsolete, still accessible via a sysprop if needed.
   Complete removal / garbage collection deferred until all needed
   functionality is ported over.

Bug: 19260237
Change-Id: I6689de3e4d14ae666d3e8da302cc9da2d4d77b9b
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index 79600f5..182a2dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -40,6 +40,7 @@
     private static final String ACTION_SET_VISIBLE = "com.android.systemui.dndtile.SET_VISIBLE";
     private static final String EXTRA_VISIBLE = "visible";
     private static final String PREF_KEY_VISIBLE = "DndTileVisible";
+    private static final String PREF_KEY_COMBINED_ICON = "DndTileCombinedIcon";
 
     private final ZenModeController mController;
     private final DndDetailAdapter mDetailAdapter;
@@ -52,7 +53,7 @@
         super(host);
         mController = host.getZenModeController();
         mDetailAdapter = new DndDetailAdapter();
-        mVisible = getSharedPrefs(mContext).getBoolean(PREF_KEY_VISIBLE, false);
+        mVisible = isVisible(host.getContext());
         mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_SET_VISIBLE));
     }
 
@@ -65,6 +66,14 @@
         return getSharedPrefs(context).getBoolean(PREF_KEY_VISIBLE, false);
     }
 
+    public static void setCombinedIcon(Context context, boolean combined) {
+        getSharedPrefs(context).edit().putBoolean(PREF_KEY_COMBINED_ICON, combined).commit();
+    }
+
+    public static boolean isCombinedIcon(Context context) {
+        return getSharedPrefs(context).getBoolean(PREF_KEY_COMBINED_ICON, false);
+    }
+
     @Override
     public DetailAdapter getDetailAdapter() {
         return mDetailAdapter;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 2236aae..1938c6d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -199,7 +199,7 @@
         int volumeIconId = 0;
         String volumeDescription = null;
 
-        if (DndTile.isVisible(mContext)) {
+        if (DndTile.isVisible(mContext) || DndTile.isCombinedIcon(mContext)) {
             zenVisible = mZen != Global.ZEN_MODE_OFF;
             zenIconId = R.drawable.stat_sys_dnd;
             zenDescription = mContext.getString(R.string.quick_settings_dnd_label);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/D.java b/packages/SystemUI/src/com/android/systemui/volume/D.java
new file mode 100644
index 0000000..db7c853
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/D.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.util.Log;
+
+class D {
+    public static boolean BUG = Log.isLoggable("volume", Log.DEBUG);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/Events.java b/packages/SystemUI/src/com/android/systemui/volume/Events.java
new file mode 100644
index 0000000..b20e39c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/Events.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.provider.Settings.Global;
+import android.util.Log;
+
+import com.android.systemui.volume.VolumeDialogController.State;
+
+import java.util.Arrays;
+
+/**
+ *  Interesting events related to the volume.
+ */
+public class Events {
+    private static final String TAG = Util.logTag(Events.class);
+
+    public static final int EVENT_SHOW_DIALOG = 0;  // (reason|int) (keyguard|bool)
+    public static final int EVENT_DISMISS_DIALOG = 1; // (reason|int)
+    public static final int EVENT_ACTIVE_STREAM_CHANGED = 2; // (stream|int)
+    public static final int EVENT_EXPAND = 3; // (expand|bool)
+    public static final int EVENT_KEY = 4;
+    public static final int EVENT_COLLECTION_STARTED = 5;
+    public static final int EVENT_COLLECTION_STOPPED = 6;
+    public static final int EVENT_ICON_CLICK = 7; // (stream|int) (icon_state|int)
+    public static final int EVENT_SETTINGS_CLICK = 8;
+    public static final int EVENT_TOUCH_LEVEL_CHANGED = 9; // (stream|int) (level|int)
+    public static final int EVENT_LEVEL_CHANGED = 10; // (stream|int) (level|int)
+    public static final int EVENT_INTERNAL_RINGER_MODE_CHANGED = 11; // (mode|int)
+    public static final int EVENT_EXTERNAL_RINGER_MODE_CHANGED = 12; // (mode|int)
+    public static final int EVENT_ZEN_MODE_CHANGED = 13; // (mode|int)
+    public static final int EVENT_SUPPRESSOR_CHANGED = 14;  // (component|string) (name|string)
+    public static final int EVENT_MUTE_CHANGED = 15;  // (stream|int) (muted|bool)
+
+    private static final String[] EVENT_TAGS = {
+        "show_dialog",
+        "dismiss_dialog",
+        "active_stream_changed",
+        "expand",
+        "key",
+        "collection_started",
+        "collection_stopped",
+        "icon_click",
+        "settings_click",
+        "touch_level_changed",
+        "level_changed",
+        "internal_ringer_mode_changed",
+        "external_ringer_mode_changed",
+        "zen_mode_changed",
+        "suppressor_changed",
+        "mute_changed",
+    };
+
+    public static final int DISMISS_REASON_UNKNOWN = 0;
+    public static final int DISMISS_REASON_TOUCH_OUTSIDE = 1;
+    public static final int DISMISS_REASON_VOLUME_CONTROLLER = 2;
+    public static final int DISMISS_REASON_TIMEOUT = 3;
+    public static final int DISMISS_REASON_SCREEN_OFF = 4;
+    public static final int DISMISS_REASON_SETTINGS_CLICKED = 5;
+    public static final int DISMISS_REASON_DONE_CLICKED = 6;
+    public static final String[] DISMISS_REASONS = {
+        "unknown",
+        "touch_outside",
+        "volume_controller",
+        "timeout",
+        "screen_off",
+        "settings_clicked",
+        "done_clicked",
+    };
+
+    public static final int SHOW_REASON_UNKNOWN = 0;
+    public static final int SHOW_REASON_VOLUME_CHANGED = 1;
+    public static final int SHOW_REASON_REMOTE_VOLUME_CHANGED = 2;
+    public static final String[] SHOW_REASONS = {
+        "unknown",
+        "volume_changed",
+        "remote_volume_changed"
+    };
+
+    public static final int ICON_STATE_UNKNOWN = 0;
+    public static final int ICON_STATE_UNMUTE = 1;
+    public static final int ICON_STATE_MUTE = 2;
+    public static final int ICON_STATE_VIBRATE = 3;
+
+    public static Callback sCallback;
+
+    public static void writeEvent(int tag, Object... list) {
+        final long time = System.currentTimeMillis();
+        final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[tag]);
+        if (list != null && list.length > 0) {
+            sb.append(" ");
+            switch (tag) {
+                case EVENT_SHOW_DIALOG:
+                    sb.append(SHOW_REASONS[(Integer) list[0]]).append(" keyguard=").append(list[1]);
+                    break;
+                case EVENT_EXPAND:
+                    sb.append(list[0]);
+                    break;
+                case EVENT_DISMISS_DIALOG:
+                    sb.append(DISMISS_REASONS[(Integer) list[0]]);
+                    break;
+                case EVENT_ACTIVE_STREAM_CHANGED:
+                    sb.append(AudioSystem.streamToString((Integer) list[0]));
+                    break;
+                case EVENT_ICON_CLICK:
+                    sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
+                            .append(iconStateToString((Integer) list[1]));
+                    break;
+                case EVENT_TOUCH_LEVEL_CHANGED:
+                case EVENT_LEVEL_CHANGED:
+                case EVENT_MUTE_CHANGED:
+                    sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
+                            .append(list[1]);
+                    break;
+                case EVENT_INTERNAL_RINGER_MODE_CHANGED:
+                case EVENT_EXTERNAL_RINGER_MODE_CHANGED:
+                    sb.append(ringerModeToString((Integer) list[0]));
+                    break;
+                case EVENT_ZEN_MODE_CHANGED:
+                    sb.append(zenModeToString((Integer) list[0]));
+                    break;
+                case EVENT_SUPPRESSOR_CHANGED:
+                    sb.append(list[0]).append(' ').append(list[1]);
+                    break;
+                default:
+                    sb.append(Arrays.asList(list));
+                    break;
+            }
+        }
+        Log.i(TAG, sb.toString());
+        if (sCallback != null) {
+            sCallback.writeEvent(time, tag, list);
+        }
+    }
+
+    public static void writeState(long time, State state) {
+        if (sCallback != null) {
+            sCallback.writeState(time, state);
+        }
+    }
+
+    private static String iconStateToString(int iconState) {
+        switch (iconState) {
+            case ICON_STATE_UNMUTE: return "unmute";
+            case ICON_STATE_MUTE: return "mute";
+            case ICON_STATE_VIBRATE: return "vibrate";
+            default: return "unknown_state_" + iconState;
+        }
+    }
+
+    private static String ringerModeToString(int ringerMode) {
+        switch (ringerMode) {
+            case AudioManager.RINGER_MODE_SILENT: return "silent";
+            case AudioManager.RINGER_MODE_VIBRATE: return "vibrate";
+            case AudioManager.RINGER_MODE_NORMAL: return "normal";
+            default: return "unknown";
+        }
+    }
+
+    private static String zenModeToString(int zenMode) {
+        switch (zenMode) {
+            case Global.ZEN_MODE_OFF: return "off";
+            case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: return "important_interruptions";
+            case Global.ZEN_MODE_NO_INTERRUPTIONS: return "no_interruptions";
+            default: return "unknown";
+        }
+    }
+
+    public interface Callback {
+        void writeEvent(long time, int tag, Object[] list);
+        void writeState(long time, State state);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java
new file mode 100644
index 0000000..712ea27
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/MediaSessions.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.media.IRemoteVolumeController;
+import android.media.MediaMetadata;
+import android.media.session.ISessionController;
+import android.media.session.MediaController;
+import android.media.session.MediaController.PlaybackInfo;
+import android.media.session.MediaSession.QueueItem;
+import android.media.session.MediaSession.Token;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Convenience client for all media session updates.  Provides a callback interface for events
+ * related to remote media sessions.
+ */
+public class MediaSessions {
+    private static final String TAG = Util.logTag(MediaSessions.class);
+
+    private static final boolean USE_SERVICE_LABEL = false;
+
+    private final Context mContext;
+    private final H mHandler;
+    private final MediaSessionManager mMgr;
+    private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>();
+    private final Callbacks mCallbacks;
+
+    private boolean mInit;
+
+    public MediaSessions(Context context, Looper looper, Callbacks callbacks) {
+        mContext = context;
+        mHandler = new H(looper);
+        mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        mCallbacks = callbacks;
+    }
+
+    public void dump(PrintWriter writer) {
+        writer.println(getClass().getSimpleName() + " state:");
+        writer.print("  mInit: "); writer.println(mInit);
+        writer.print("  mRecords.size: "); writer.println(mRecords.size());
+        int i = 0;
+        for (MediaControllerRecord r : mRecords.values()) {
+            dump(++i, writer, r.controller);
+        }
+    }
+
+    public void init() {
+        if (D.BUG) Log.d(TAG, "init");
+        // will throw if no permission
+        mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler);
+        mInit = true;
+        postUpdateSessions();
+        mMgr.setRemoteVolumeController(mRvc);
+    }
+
+    protected void postUpdateSessions() {
+        if (!mInit) return;
+        mHandler.sendEmptyMessage(H.UPDATE_SESSIONS);
+    }
+
+    public void destroy() {
+        if (D.BUG) Log.d(TAG, "destroy");
+        mInit = false;
+        mMgr.removeOnActiveSessionsChangedListener(mSessionsListener);
+    }
+
+    public void setVolume(Token token, int level) {
+        final MediaControllerRecord r = mRecords.get(token);
+        if (r == null) {
+            Log.w(TAG, "setVolume: No record found for token " + token);
+            return;
+        }
+        if (D.BUG) Log.d(TAG, "Setting level to " + level);
+        r.controller.setVolumeTo(level, 0);
+    }
+
+    private void onRemoteVolumeChangedH(ISessionController session, int flags) {
+        final MediaController controller = new MediaController(mContext, session);
+        if (D.BUG) Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " "
+                + Util.audioManagerFlagsToString(flags));
+        final Token token = controller.getSessionToken();
+        mCallbacks.onRemoteVolumeChanged(token, flags);
+    }
+
+    private void onUpdateRemoteControllerH(ISessionController session) {
+        final MediaController controller = session != null ? new MediaController(mContext, session)
+                : null;
+        final String pkg = controller != null ? controller.getPackageName() : null;
+        if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg);
+        // this may be our only indication that a remote session is changed, refresh
+        postUpdateSessions();
+    }
+
+    protected void onActiveSessionsUpdatedH(List<MediaController> controllers) {
+        if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size());
+        final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet());
+        for (MediaController controller : controllers) {
+            final Token token = controller.getSessionToken();
+            final PlaybackInfo pi = controller.getPlaybackInfo();
+            toRemove.remove(token);
+            if (!mRecords.containsKey(token)) {
+                final MediaControllerRecord r = new MediaControllerRecord(controller);
+                r.name = getControllerName(controller);
+                mRecords.put(token, r);
+                controller.registerCallback(r, mHandler);
+            }
+            final MediaControllerRecord r = mRecords.get(token);
+            final boolean remote = isRemote(pi);
+            if (remote) {
+                updateRemoteH(token, r.name, pi);
+                r.sentRemote = true;
+            }
+        }
+        for (Token t : toRemove) {
+            final MediaControllerRecord r = mRecords.get(t);
+            r.controller.unregisterCallback(r);
+            mRecords.remove(t);
+            if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote);
+            if (r.sentRemote) {
+                mCallbacks.onRemoteRemoved(t);
+                r.sentRemote = false;
+            }
+        }
+    }
+
+    private static boolean isRemote(PlaybackInfo pi) {
+        return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+    }
+
+    protected String getControllerName(MediaController controller) {
+        final PackageManager pm = mContext.getPackageManager();
+        final String pkg = controller.getPackageName();
+        try {
+            if (USE_SERVICE_LABEL) {
+                final List<ResolveInfo> ris = pm.queryIntentServices(
+                        new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0);
+                if (ris != null) {
+                    for (ResolveInfo ri : ris) {
+                        if (ri.serviceInfo == null) continue;
+                        if (pkg.equals(ri.serviceInfo.packageName)) {
+                            final String serviceLabel =
+                                    Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim();
+                            if (serviceLabel.length() > 0) {
+                                return serviceLabel;
+                            }
+                        }
+                    }
+                }
+            }
+            final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
+            final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim();
+            if (appLabel.length() > 0) {
+                return appLabel;
+            }
+        } catch (NameNotFoundException e) { }
+        return pkg;
+    }
+
+    private void updateRemoteH(Token token, String name, PlaybackInfo pi) {
+        if (mCallbacks != null) {
+            mCallbacks.onRemoteUpdate(token, name, pi);
+        }
+    }
+
+    private static void dump(int n, PrintWriter writer, MediaController c) {
+        writer.println("  Controller " + n + ": " + c.getPackageName());
+        final Bundle extras = c.getExtras();
+        final long flags = c.getFlags();
+        final MediaMetadata mm = c.getMetadata();
+        final PlaybackInfo pi = c.getPlaybackInfo();
+        final PlaybackState playbackState = c.getPlaybackState();
+        final List<QueueItem> queue = c.getQueue();
+        final CharSequence queueTitle = c.getQueueTitle();
+        final int ratingType = c.getRatingType();
+        final PendingIntent sessionActivity = c.getSessionActivity();
+
+        writer.println("    PlaybackState: " + Util.playbackStateToString(playbackState));
+        writer.println("    PlaybackInfo: " + Util.playbackInfoToString(pi));
+        if (mm != null) {
+            writer.println("  MediaMetadata.desc=" + mm.getDescription());
+        }
+        writer.println("    RatingType: " + ratingType);
+        writer.println("    Flags: " + flags);
+        if (extras != null) {
+            writer.println("    Extras:");
+            for (String key : extras.keySet()) {
+                writer.println("      " + key + "=" + extras.get(key));
+            }
+        }
+        if (queueTitle != null) {
+            writer.println("    QueueTitle: " + queueTitle);
+        }
+        if (queue != null && !queue.isEmpty()) {
+            writer.println("    Queue:");
+            for (QueueItem qi : queue) {
+                writer.println("      " + qi);
+            }
+        }
+        if (pi != null) {
+            writer.println("    sessionActivity: " + sessionActivity);
+        }
+    }
+
+    public static void dumpMediaSessions(Context context) {
+        final MediaSessionManager mgr = (MediaSessionManager) context
+                .getSystemService(Context.MEDIA_SESSION_SERVICE);
+        try {
+            final List<MediaController> controllers = mgr.getActiveSessions(null);
+            final int N = controllers.size();
+            if (D.BUG) Log.d(TAG, N + " controllers");
+            for (int i = 0; i < N; i++) {
+                final StringWriter sw = new StringWriter();
+                final PrintWriter pw = new PrintWriter(sw, true);
+                dump(i + 1, pw, controllers.get(i));
+                if (D.BUG) Log.d(TAG, sw.toString());
+            }
+        } catch (SecurityException e) {
+            Log.w(TAG, "Not allowed to get sessions", e);
+        }
+    }
+
+    private final class MediaControllerRecord extends MediaController.Callback {
+        private final MediaController controller;
+
+        private boolean sentRemote;
+        private String name;
+
+        private MediaControllerRecord(MediaController controller) {
+            this.controller = controller;
+        }
+
+        private String cb(String method) {
+            return method + " " + controller.getPackageName() + " ";
+        }
+
+        @Override
+        public void onAudioInfoChanged(PlaybackInfo info) {
+            if (D.BUG) Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info)
+                    + " sentRemote=" + sentRemote);
+            final boolean remote = isRemote(info);
+            if (!remote && sentRemote) {
+                mCallbacks.onRemoteRemoved(controller.getSessionToken());
+                sentRemote = false;
+            } else if (remote) {
+                updateRemoteH(controller.getSessionToken(), name, info);
+                sentRemote = true;
+            }
+        }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras);
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata));
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state));
+        }
+
+        @Override
+        public void onQueueChanged(List<QueueItem> queue) {
+            if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue);
+        }
+
+        @Override
+        public void onQueueTitleChanged(CharSequence title) {
+            if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title);
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            if (D.BUG) Log.d(TAG, cb("onSessionDestroyed"));
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras);
+        }
+    }
+
+    private final OnActiveSessionsChangedListener mSessionsListener =
+            new OnActiveSessionsChangedListener() {
+        @Override
+        public void onActiveSessionsChanged(List<MediaController> controllers) {
+            onActiveSessionsUpdatedH(controllers);
+        }
+    };
+
+    private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() {
+        @Override
+        public void remoteVolumeChanged(ISessionController session, int flags)
+                throws RemoteException {
+            mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0, session).sendToTarget();
+        }
+
+        @Override
+        public void updateRemoteController(final ISessionController session)
+                throws RemoteException {
+            mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, session).sendToTarget();
+        }
+    };
+
+    private final class H extends Handler {
+        private static final int UPDATE_SESSIONS = 1;
+        private static final int REMOTE_VOLUME_CHANGED = 2;
+        private static final int UPDATE_REMOTE_CONTROLLER = 3;
+
+        private H(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case UPDATE_SESSIONS:
+                    onActiveSessionsUpdatedH(mMgr.getActiveSessions(null));
+                    break;
+                case REMOTE_VOLUME_CHANGED:
+                    onRemoteVolumeChangedH((ISessionController) msg.obj, msg.arg1);
+                    break;
+                case UPDATE_REMOTE_CONTROLLER:
+                    onUpdateRemoteControllerH((ISessionController) msg.obj);
+                    break;
+            }
+        }
+    }
+
+    public interface Callbacks {
+        void onRemoteUpdate(Token token, String name, PlaybackInfo pi);
+        void onRemoteRemoved(Token t);
+        void onRemoteVolumeChanged(Token token, int flags);
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/Prefs.java b/packages/SystemUI/src/com/android/systemui/volume/Prefs.java
new file mode 100644
index 0000000..58bc9f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/Prefs.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.PreferenceManager;
+
+/**
+ *  Configuration for the volume dialog + related policy.
+ */
+public class Prefs {
+
+    public static final String PREF_ENABLE_PROTOTYPE = "pref_enable_prototype";  // not persistent
+    public static final String PREF_SHOW_ALARMS = "pref_show_alarms";
+    public static final String PREF_SHOW_SYSTEM = "pref_show_system";
+    public static final String PREF_SHOW_HEADERS = "pref_show_headers";
+    public static final String PREF_SHOW_FAKE_REMOTE_1 = "pref_show_fake_remote_1";
+    public static final String PREF_SHOW_FAKE_REMOTE_2 = "pref_show_fake_remote_2";
+    public static final String PREF_SHOW_FOOTER = "pref_show_footer";
+    public static final String PREF_ZEN_FOOTER = "pref_zen_footer";
+    public static final String PREF_ENABLE_AUTOMUTE = "pref_enable_automute";
+    public static final String PREF_ENABLE_SILENT_MODE = "pref_enable_silent_mode";
+    public static final String PREF_DEBUG_LOGGING = "pref_debug_logging";
+    public static final String PREF_SEND_LOGS = "pref_send_logs";
+    public static final String PREF_ADJUST_SYSTEM = "pref_adjust_system";
+    public static final String PREF_ADJUST_VOICE_CALLS = "pref_adjust_voice_calls";
+    public static final String PREF_ADJUST_BLUETOOTH_SCO = "pref_adjust_bluetooth_sco";
+    public static final String PREF_ADJUST_MEDIA = "pref_adjust_media";
+    public static final String PREF_ADJUST_ALARMS = "pref_adjust_alarms";
+    public static final String PREF_ADJUST_NOTIFICATION = "pref_adjust_notification";
+
+    public static final boolean DEFAULT_SHOW_HEADERS = true;
+    public static final boolean DEFAULT_SHOW_FOOTER = true;
+    public static final boolean DEFAULT_ENABLE_AUTOMUTE = true;
+    public static final boolean DEFAULT_ENABLE_SILENT_MODE = true;
+    public static final boolean DEFAULT_ZEN_FOOTER = true;
+
+    public static void unregisterCallbacks(Context c, OnSharedPreferenceChangeListener listener) {
+        prefs(c).unregisterOnSharedPreferenceChangeListener(listener);
+    }
+
+    public static void registerCallbacks(Context c, OnSharedPreferenceChangeListener listener) {
+        prefs(c).registerOnSharedPreferenceChangeListener(listener);
+    }
+
+    private static SharedPreferences prefs(Context context) {
+        return PreferenceManager.getDefaultSharedPreferences(context);
+    }
+
+    public static boolean get(Context context, String key, boolean def) {
+        return prefs(context).getBoolean(key, def);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java
index 5f5b881..4f20ac7 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/SegmentedButtons.java
@@ -60,17 +60,14 @@
             final Object tag = c.getTag();
             final boolean selected = Objects.equals(mSelectedValue, tag);
             c.setSelected(selected);
-            c.getCompoundDrawables()[1].setTint(mContext.getColor(selected
-                    ? R.color.segmented_button_selected : R.color.segmented_button_unselected));
         }
         fireOnSelected();
     }
 
-    public void addButton(int labelResId, int iconResId, Object value) {
+    public void addButton(int labelResId, Object value) {
         final Button b = (Button) mInflater.inflate(R.layout.segmented_button, this, false);
         b.setTag(LABEL_RES_KEY, labelResId);
         b.setText(labelResId);
-        b.setCompoundDrawablesWithIntrinsicBounds(0, iconResId, 0, 0);
         final LayoutParams lp = (LayoutParams) b.getLayoutParams();
         if (getChildCount() == 0) {
             lp.leftMargin = lp.rightMargin = 0; // first button has no margin
diff --git a/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java
new file mode 100644
index 0000000..d8e53db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/SpTexts.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.ArrayMap;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.widget.TextView;
+
+/**
+ * Capture initial sp values for registered textviews, and update properly when configuration
+ * changes.
+ */
+public class SpTexts {
+
+    private final Context mContext;
+    private final ArrayMap<TextView, Integer> mTexts = new ArrayMap<>();
+
+    public SpTexts(Context context) {
+        mContext = context;
+    }
+
+    public int add(final TextView text) {
+        if (text == null) return 0;
+        final Resources res = mContext.getResources();
+        final float fontScale = res.getConfiguration().fontScale;
+        final float density = res.getDisplayMetrics().density;
+        final float px = text.getTextSize();
+        final int sp = (int)(px / fontScale / density);
+        mTexts.put(text, sp);
+        text.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+            }
+
+            @Override
+            public void onViewAttachedToWindow(View v) {
+               setTextSizeH(text, sp);
+            }
+        });
+        return sp;
+    }
+
+    public void update() {
+        if (mTexts.isEmpty()) return;
+        mTexts.keyAt(0).post(mUpdateAll);
+    }
+
+    private void setTextSizeH(TextView text, int sp) {
+        text.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp);
+    }
+
+    private final Runnable mUpdateAll = new Runnable() {
+        @Override
+        public void run() {
+            for (int i = 0; i < mTexts.size(); i++) {
+                setTextSizeH(mTexts.keyAt(i), mTexts.valueAt(i));
+            }
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/Util.java b/packages/SystemUI/src/com/android/systemui/volume/Util.java
new file mode 100644
index 0000000..fbdc1ca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/Util.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.VolumeProvider;
+import android.media.session.MediaController.PlaybackInfo;
+import android.media.session.PlaybackState;
+import android.service.notification.ZenModeConfig.DowntimeInfo;
+import android.view.View;
+import android.widget.TextView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Static helpers for the volume dialog.
+ */
+class Util {
+
+    // Note: currently not shown (only used in the text footer)
+    private static final SimpleDateFormat HMMAA = new SimpleDateFormat("h:mm aa", Locale.US);
+
+    private static int[] AUDIO_MANAGER_FLAGS = new int[] {
+        AudioManager.FLAG_SHOW_UI,
+        AudioManager.FLAG_VIBRATE,
+        AudioManager.FLAG_PLAY_SOUND,
+        AudioManager.FLAG_ALLOW_RINGER_MODES,
+        AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
+        AudioManager.FLAG_SHOW_VIBRATE_HINT,
+        AudioManager.FLAG_SHOW_SILENT_HINT,
+        AudioManager.FLAG_FROM_KEY,
+    };
+
+    private static String[] AUDIO_MANAGER_FLAG_NAMES = new String[] {
+        "SHOW_UI",
+        "VIBRATE",
+        "PLAY_SOUND",
+        "ALLOW_RINGER_MODES",
+        "REMOVE_SOUND_AND_VIBRATE",
+        "SHOW_VIBRATE_HINT",
+        "SHOW_SILENT_HINT",
+        "FROM_KEY",
+    };
+
+    public static String logTag(Class<?> c) {
+        final String tag = "vol." + c.getSimpleName();
+        return tag.length() < 23 ? tag : tag.substring(0, 23);
+    }
+
+    public static String ringerModeToString(int ringerMode) {
+        switch (ringerMode) {
+            case AudioManager.RINGER_MODE_SILENT: return "RINGER_MODE_SILENT";
+            case AudioManager.RINGER_MODE_VIBRATE: return "RINGER_MODE_VIBRATE";
+            case AudioManager.RINGER_MODE_NORMAL: return "RINGER_MODE_NORMAL";
+            default: return "RINGER_MODE_UNKNOWN_" + ringerMode;
+        }
+    }
+
+    public static String mediaMetadataToString(MediaMetadata metadata) {
+        return metadata.getDescription().toString();
+    }
+
+    public static String playbackInfoToString(PlaybackInfo info) {
+        if (info == null) return null;
+        final String type = playbackInfoTypeToString(info.getPlaybackType());
+        final String vc = volumeProviderControlToString(info.getVolumeControl());
+        return String.format("PlaybackInfo[vol=%s,max=%s,type=%s,vc=%s],atts=%s",
+                info.getCurrentVolume(), info.getMaxVolume(), type, vc, info.getAudioAttributes());
+    }
+
+    public static String playbackInfoTypeToString(int type) {
+        switch (type) {
+            case PlaybackInfo.PLAYBACK_TYPE_LOCAL: return "LOCAL";
+            case PlaybackInfo.PLAYBACK_TYPE_REMOTE: return "REMOTE";
+            default: return "UNKNOWN_" + type;
+        }
+    }
+
+    public static String playbackStateStateToString(int state) {
+        switch (state) {
+            case PlaybackState.STATE_NONE: return "STATE_NONE";
+            case PlaybackState.STATE_STOPPED: return "STATE_STOPPED";
+            case PlaybackState.STATE_PAUSED: return "STATE_PAUSED";
+            case PlaybackState.STATE_PLAYING: return "STATE_PLAYING";
+            default: return "UNKNOWN_" + state;
+        }
+    }
+
+    public static String volumeProviderControlToString(int control) {
+        switch (control) {
+            case VolumeProvider.VOLUME_CONTROL_ABSOLUTE: return "VOLUME_CONTROL_ABSOLUTE";
+            case VolumeProvider.VOLUME_CONTROL_FIXED: return "VOLUME_CONTROL_FIXED";
+            case VolumeProvider.VOLUME_CONTROL_RELATIVE: return "VOLUME_CONTROL_RELATIVE";
+            default: return "VOLUME_CONTROL_UNKNOWN_" + control;
+        }
+    }
+
+    public static String playbackStateToString(PlaybackState playbackState) {
+        if (playbackState == null) return null;
+        return playbackStateStateToString(playbackState.getState()) + " " + playbackState;
+    }
+
+    public static String audioManagerFlagsToString(int value) {
+        return bitFieldToString(value, AUDIO_MANAGER_FLAGS, AUDIO_MANAGER_FLAG_NAMES);
+    }
+
+    private static String bitFieldToString(int value, int[] values, String[] names) {
+        if (value == 0) return "";
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if ((value & values[i]) != 0) {
+                if (sb.length() > 0) sb.append(',');
+                sb.append(names[i]);
+            }
+            value &= ~values[i];
+        }
+        if (value != 0) {
+            if (sb.length() > 0) sb.append(',');
+            sb.append("UNKNOWN_").append(value);
+        }
+        return sb.toString();
+    }
+
+    public static String getShortTime(long millis) {
+        return HMMAA.format(new Date(millis));
+    }
+
+    public static String getShortTime(DowntimeInfo info) {
+        return ((info.endHour + 1) % 12) + ":" + (info.endMinute < 10 ? " " : "") + info.endMinute;
+    }
+
+    public static void setText(TextView tv, CharSequence text) {
+        if (Objects.equals(tv.getText(), text)) return;
+        tv.setText(text);
+    }
+
+    public static final void setVisOrGone(View v, boolean vis) {
+        if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return;
+        v.setVisibility(vis ? View.VISIBLE : View.GONE);
+    }
+
+    public static final void setVisOrInvis(View v, boolean vis) {
+        if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return;
+        v.setVisibility(vis ? View.VISIBLE : View.INVISIBLE);
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java
index e3f8f3d..1f0ee57 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeComponent.java
@@ -16,10 +16,18 @@
 
 package com.android.systemui.volume;
 
+import android.content.res.Configuration;
+
 import com.android.systemui.DemoMode;
 import com.android.systemui.statusbar.policy.ZenModeController;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
 public interface VolumeComponent extends DemoMode {
     ZenModeController getZenController();
     void dismissNow();
+    void onConfigurationChanged(Configuration newConfig);
+    void dump(FileDescriptor fd, PrintWriter pw, String[] args);
+    void register();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
new file mode 100644
index 0000000..5fbb18d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
@@ -0,0 +1,1039 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.animation.LayoutTransition;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.Settings.Global;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.DowntimeInfo;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.ZenModeController;
+import com.android.systemui.volume.VolumeDialogController.State;
+import com.android.systemui.volume.VolumeDialogController.StreamState;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Visual presentation of the volume dialog.
+ *
+ * A client of VolumeDialogController and its state model.
+ *
+ * Methods ending in "H" must be called on the (ui) handler.
+ */
+public class VolumeDialog {
+    private static final String TAG = Util.logTag(VolumeDialog.class);
+
+    private static final long USER_ATTEMPT_GRACE_PERIOD = 1000;
+    private static final int WAIT_FOR_RIPPLE = 200;
+    private static final int UPDATE_ANIMATION_DURATION = 80;
+
+    private final Context mContext;
+    private final H mHandler = new H();
+    private final VolumeDialogController mController;
+
+    private final CustomDialog mDialog;
+    private final ViewGroup mDialogView;
+    private final ViewGroup mDialogContentView;
+    private final ImageButton mExpandButton;
+    private final TextView mFootlineText;
+    private final Button mFootlineAction;
+    private final View mSettingsButton;
+    private final View mFooter;
+    private final List<VolumeRow> mRows = new ArrayList<VolumeRow>();
+    private final SpTexts mSpTexts;
+    private final SparseBooleanArray mDynamic = new SparseBooleanArray();
+    private final KeyguardManager mKeyguard;
+    private final int mExpandButtonAnimationDuration;
+    private final View mTextFooter;
+    private final ZenFooter mZenFooter;
+    private final LayoutTransition mLayoutTransition;
+
+    private boolean mShowing;
+    private boolean mExpanded;
+    private int mActiveStream;
+    private boolean mShowHeaders = Prefs.DEFAULT_SHOW_HEADERS;
+    private boolean mShowFooter = Prefs.DEFAULT_SHOW_FOOTER;
+    private boolean mShowZenFooter = Prefs.DEFAULT_ZEN_FOOTER;
+    private boolean mAutomute = Prefs.DEFAULT_ENABLE_AUTOMUTE;
+    private boolean mSilentMode = Prefs.DEFAULT_ENABLE_SILENT_MODE;
+    private State mState;
+    private int mExpandAnimRes;
+    private boolean mExpanding;
+
+    public VolumeDialog(Context context, VolumeDialogController controller,
+            ZenModeController zenModeController) {
+        mContext = context;
+        mController = controller;
+        mSpTexts = new SpTexts(mContext);
+        mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+
+        mDialog = new CustomDialog(mContext);
+
+        final Window window = mDialog.getWindow();
+        window.requestFeature(Window.FEATURE_NO_TITLE);
+        window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+        window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+        mDialog.setCanceledOnTouchOutside(true);
+        final Resources res = mContext.getResources();
+        final WindowManager.LayoutParams lp = window.getAttributes();
+        lp.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
+        lp.format = PixelFormat.TRANSLUCENT;
+        lp.setTitle(VolumeDialog.class.getSimpleName());
+        lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
+        lp.windowAnimations = R.style.VolumeDialogAnimations;
+        lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top);
+        lp.gravity = Gravity.TOP;
+        window.setAttributes(lp);
+
+        mDialog.setContentView(R.layout.volume_dialog);
+        mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog);
+        mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content);
+        mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button);
+        mExpandButton.setOnClickListener(mClickExpand);
+        updateWindowWidthH();
+        updateExpandButtonH();
+        mLayoutTransition = new LayoutTransition();
+        mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
+        mLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING);
+        mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+        mDialogContentView.setLayoutTransition(mLayoutTransition);
+
+        addRow(AudioManager.STREAM_RING,
+                R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
+        addRow(AudioManager.STREAM_MUSIC,
+                R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true);
+        addRow(AudioManager.STREAM_ALARM,
+                R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false);
+        addRow(AudioManager.STREAM_VOICE_CALL,
+                R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false);
+        addRow(AudioManager.STREAM_BLUETOOTH_SCO,
+                R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false);
+        addRow(AudioManager.STREAM_SYSTEM,
+                R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false);
+
+        mTextFooter = mDialog.findViewById(R.id.volume_text_footer);
+        mFootlineText = (TextView) mDialog.findViewById(R.id.volume_footline_text);
+        mSpTexts.add(mFootlineText);
+        mFootlineAction = (Button) mDialog.findViewById(R.id.volume_footline_action_button);
+        mSpTexts.add(mFootlineAction);
+        mFooter = mDialog.findViewById(R.id.volume_footer);
+        mSettingsButton = mDialog.findViewById(R.id.volume_settings_button);
+        mSettingsButton.setOnClickListener(mClickSettings);
+        mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration);
+        mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer);
+        mZenFooter.init(zenModeController, mZenFooterCallback);
+
+        controller.addCallback(mControllerCallbackH, mHandler);
+        controller.getState();
+    }
+
+    private void updateWindowWidthH() {
+        final ViewGroup.LayoutParams lp = mDialogView.getLayoutParams();
+        final DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
+        if (D.BUG) Log.d(TAG, "updateWindowWidth dm.w=" + dm.widthPixels);
+        int w = dm.widthPixels;
+        final int max = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.standard_notification_panel_width);
+        if (w > max) {
+            w = max;
+        }
+        w -= mContext.getResources().getDimensionPixelSize(R.dimen.notification_side_padding) * 2;
+        lp.width = w;
+        mDialogView.setLayoutParams(lp);
+    }
+
+    public void setStreamImportant(int stream, boolean important) {
+        mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget();
+    }
+
+    public void setShowHeaders(boolean showHeaders) {
+        if (showHeaders == mShowHeaders) return;
+        mShowHeaders = showHeaders;
+        mHandler.sendEmptyMessage(H.RECHECK_ALL);
+    }
+
+    public void setShowFooter(boolean show) {
+        if (mShowFooter == show) return;
+        mShowFooter = show;
+        mHandler.sendEmptyMessage(H.RECHECK_ALL);
+    }
+
+    public void setZenFooter(boolean zen) {
+        if (mShowZenFooter == zen) return;
+        mShowZenFooter = zen;
+        mHandler.sendEmptyMessage(H.RECHECK_ALL);
+    }
+
+    public void setAutomute(boolean automute) {
+        if (mAutomute == automute) return;
+        mAutomute = automute;
+        mHandler.sendEmptyMessage(H.RECHECK_ALL);
+    }
+
+    public void setSilentMode(boolean silentMode) {
+        if (mSilentMode == silentMode) return;
+        mSilentMode = silentMode;
+        mHandler.sendEmptyMessage(H.RECHECK_ALL);
+    }
+
+    private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) {
+        final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important);
+        if (!mRows.isEmpty()) {
+            final View v = new View(mContext);
+            final int h = mContext.getResources()
+                    .getDimensionPixelSize(R.dimen.volume_slider_interspacing);
+            final LinearLayout.LayoutParams lp =
+                    new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h);
+            mDialogContentView.addView(v, mDialogContentView.getChildCount() - 1, lp);
+            row.space = v;
+        }
+        row.settingsButton.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                if (D.BUG) Log.d(TAG, "onLayoutChange"
+                        + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString()
+                        + " new=" + new Rect(left,top,right,bottom).toShortString());
+                if (oldLeft != left || oldTop != top) {
+                    for (int i = 0; i < mDialogContentView.getChildCount(); i++) {
+                        final View c = mDialogContentView.getChildAt(i);
+                        if (!c.isShown()) continue;
+                        if (c == row.view) {
+                            repositionExpandAnim(row);
+                        }
+                        return;
+                    }
+                }
+            }
+        });
+        // add new row just before the footer
+        mDialogContentView.addView(row.view, mDialogContentView.getChildCount() - 1);
+        mRows.add(row);
+    }
+
+    private boolean isAttached() {
+        return mDialogContentView != null && mDialogContentView.isAttachedToWindow();
+    }
+
+    private VolumeRow getActiveRow() {
+        for (VolumeRow row : mRows) {
+            if (row.stream == mActiveStream) {
+                return row;
+            }
+        }
+        return mRows.get(0);
+    }
+
+    private VolumeRow findRow(int stream) {
+        for (VolumeRow row : mRows) {
+            if (row.stream == stream) return row;
+        }
+        return null;
+    }
+
+    private void repositionExpandAnim(VolumeRow row) {
+        final int[] loc = new int[2];
+        row.settingsButton.getLocationInWindow(loc);
+        final MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams();
+        final int x = loc[0] - mlp.leftMargin;
+        final int y = loc[1] - mlp.topMargin;
+        if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y);
+        mExpandButton.setTranslationX(x);
+        mExpandButton.setTranslationY(y);
+    }
+
+    public void dump(PrintWriter writer) {
+        writer.println(VolumeDialog.class.getSimpleName() + " state:");
+        writer.print("  mShowing: "); writer.println(mShowing);
+        writer.print("  mExpanded: "); writer.println(mExpanded);
+        writer.print("  mExpanding: "); writer.println(mExpanding);
+        writer.print("  mActiveStream: "); writer.println(mActiveStream);
+        writer.print("  mDynamic: "); writer.println(mDynamic);
+        writer.print("  mShowHeaders: "); writer.println(mShowHeaders);
+        writer.print("  mShowFooter: "); writer.println(mShowFooter);
+        writer.print("  mAutomute: "); writer.println(mAutomute);
+        writer.print("  mSilentMode: "); writer.println(mSilentMode);
+    }
+
+    private static int getImpliedLevel(SeekBar seekBar, int progress) {
+        final int m = seekBar.getMax();
+        final int n = m / 100 - 1;
+        final int level = progress == 0 ? 0
+                : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n));
+        return level;
+    }
+
+    @SuppressLint("InflateParams")
+    private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) {
+        final VolumeRow row = new VolumeRow();
+        row.stream = stream;
+        row.iconRes = iconRes;
+        row.iconMuteRes = iconMuteRes;
+        row.important = important;
+        row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null);
+        row.view.setTag(row);
+        row.header = (TextView) row.view.findViewById(R.id.volume_row_header);
+        mSpTexts.add(row.header);
+        row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider);
+        row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
+
+        // forward events above the slider into the slider
+        row.view.setOnTouchListener(new OnTouchListener() {
+            private final Rect mSliderHitRect = new Rect();
+            private boolean mDragging;
+
+            @SuppressLint("ClickableViewAccessibility")
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                row.slider.getHitRect(mSliderHitRect);
+                if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN
+                        && event.getY() < mSliderHitRect.top) {
+                    mDragging = true;
+                }
+                if (mDragging) {
+                    event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top);
+                    row.slider.dispatchTouchEvent(event);
+                    if (event.getActionMasked() == MotionEvent.ACTION_UP
+                            || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+                        mDragging = false;
+                    }
+                    return true;
+                }
+                return false;
+            }
+        });
+        row.icon = (ImageButton) row.view.findViewById(R.id.volume_row_icon);
+        row.icon.setImageResource(iconRes);
+        row.icon.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Events.writeEvent(Events.EVENT_ICON_CLICK, row.stream, row.iconState);
+                mController.setActiveStream(row.stream);
+                if (row.stream == AudioManager.STREAM_RING) {
+                    final boolean hasVibrator = mController.hasVibrator();
+                    if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
+                        if (hasVibrator) {
+                            mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
+                        } else {
+                            final boolean wasZero = row.ss.level == 0;
+                            mController.setStreamVolume(stream, wasZero ? row.lastAudibleLevel : 0);
+                        }
+                    } else {
+                        mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
+                        if (row.ss.level == 0) {
+                            mController.setStreamVolume(stream, 1);
+                        }
+                    }
+                } else {
+                    if (mAutomute && !row.ss.muteSupported) {
+                        final boolean vmute = row.ss.level == 0;
+                        mController.setStreamVolume(stream, vmute ? row.lastAudibleLevel : 0);
+                    } else {
+                        final boolean mute = !row.ss.muted;
+                        mController.setStreamMute(stream, mute);
+                        if (mAutomute) {
+                            if (!mute && row.ss.level == 0) {
+                                mController.setStreamVolume(stream, 1);
+                            }
+                        }
+                    }
+                }
+                row.userAttempt = 0;  // reset the grace period, slider should update immediately
+            }
+        });
+        row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button);
+        row.settingsButton.setOnClickListener(mClickSettings);
+        return row;
+    }
+
+    public void destroy() {
+        mController.removeCallback(mControllerCallbackH);
+    }
+
+    public void show(int reason) {
+        mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget();
+    }
+
+    public void dismiss(int reason) {
+        mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
+    }
+
+    protected void onSettingsClickedH() {
+        // hook for subclasses
+    }
+
+    protected void onZenSettingsClickedH() {
+        // hook for subclasses
+    }
+
+    private void showH(int reason) {
+        mHandler.removeMessages(H.SHOW);
+        mHandler.removeMessages(H.DISMISS);
+        rescheduleTimeoutH();
+        if (mShowing) return;
+        mShowing = true;
+        mDialog.show();
+        Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
+        mController.notifyVisible(true);
+    }
+
+    protected void rescheduleTimeoutH() {
+        mHandler.removeMessages(H.DISMISS);
+        final int timeout = computeTimeoutH();
+        if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout);
+        mHandler.sendMessageDelayed(mHandler
+                .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout);
+        mController.userActivity();
+    }
+
+    private int computeTimeoutH() {
+        if (mZenFooter != null && mZenFooter.isFooterExpanded()) return 10000;
+        if (mExpanded || mExpanding) return 5000;
+        if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500;
+        return 3000;
+    }
+
+    protected void dismissH(int reason) {
+        mHandler.removeMessages(H.DISMISS);
+        mHandler.removeMessages(H.SHOW);
+        if (!mShowing) return;
+        mShowing = false;
+        mDialog.dismiss();
+        Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason);
+        setExpandedH(false);
+        mController.notifyVisible(false);
+    }
+
+    private void setExpandedH(boolean expanded) {
+        if (mExpanded == expanded) return;
+        mExpanded = expanded;
+        mExpanding = isAttached();
+        if (D.BUG) Log.d(TAG, "setExpandedH " + expanded);
+        updateRowsH();
+        if (mExpanding) {
+            final Drawable d = mExpandButton.getDrawable();
+            if (d instanceof AnimatedVectorDrawable) {
+                // workaround to reset drawable
+                final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState()
+                        .newDrawable();
+                mExpandButton.setImageDrawable(avd);
+                avd.start();
+                mHandler.postDelayed(new Runnable() {
+                    @Override
+                    public void run() {
+                        mExpanding = false;
+                        updateExpandButtonH();
+                        rescheduleTimeoutH();
+                    }
+                }, mExpandButtonAnimationDuration);
+            }
+        }
+        rescheduleTimeoutH();
+    }
+
+    private void updateExpandButtonH() {
+        mExpandButton.setClickable(!mExpanding);
+        if (mExpanding && isAttached()) return;
+        final int res = mExpanded ? R.drawable.ic_volume_collapse_animation
+                : R.drawable.ic_volume_expand_animation;
+        if (res == mExpandAnimRes) return;
+        mExpandAnimRes = res;
+        mExpandButton.setImageResource(res);
+    }
+
+    private boolean isVisibleH(VolumeRow row, boolean isActive) {
+        return mExpanded && row.view.getVisibility() == View.VISIBLE
+                || (mExpanded && (row.important || isActive))
+                || !mExpanded && isActive;
+    }
+
+    private void updateRowsH() {
+        final VolumeRow activeRow = getActiveRow();
+        updateFooterH();
+        updateExpandButtonH();
+        final boolean footerVisible = mFooter.getVisibility() == View.VISIBLE;
+        if (!mShowing) {
+            trimObsoleteH();
+        }
+        // first, find the last visible row
+        VolumeRow lastVisible = null;
+        for (VolumeRow row : mRows) {
+            final boolean isActive = row == activeRow;
+            if (isVisibleH(row, isActive)) {
+                lastVisible = row;
+            }
+        }
+        // apply changes to all rows
+        for (VolumeRow row : mRows) {
+            final boolean isActive = row == activeRow;
+            final boolean visible = isVisibleH(row, isActive);
+            Util.setVisOrGone(row.view, visible);
+            Util.setVisOrGone(row.space, visible && mExpanded);
+            final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0;
+            if (expandButtonRes != row.cachedExpandButtonRes) {
+                row.cachedExpandButtonRes = expandButtonRes;
+                if (expandButtonRes == 0) {
+                    row.settingsButton.setImageDrawable(null);
+                } else {
+                    row.settingsButton.setImageResource(expandButtonRes);
+                }
+            }
+            Util.setVisOrInvis(row.settingsButton,
+                     mExpanded && (!footerVisible && row == lastVisible));
+            row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f);
+        }
+    }
+
+    private void trimObsoleteH() {
+        for (int i = mRows.size() -1; i >= 0; i--) {
+            final VolumeRow row = mRows.get(i);
+            if (row.ss == null || !row.ss.dynamic) continue;
+            if (!mDynamic.get(row.stream)) {
+                mRows.remove(i);
+                mDialogContentView.removeView(row.view);
+                mDialogContentView.removeView(row.space);
+            }
+        }
+    }
+
+    private void onStateChangedH(State state) {
+        mState = state;
+        mDynamic.clear();
+        // add any new dynamic rows
+        for (int i = 0; i < state.states.size(); i++) {
+            final int stream = state.states.keyAt(i);
+            final StreamState ss = state.states.valueAt(i);
+            if (!ss.dynamic) continue;
+            mDynamic.put(stream, true);
+            if (findRow(stream) == null) {
+                addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true);
+            }
+        }
+
+        if (mActiveStream != state.activeStream) {
+            mActiveStream = state.activeStream;
+            updateRowsH();
+            rescheduleTimeoutH();
+        }
+        for (VolumeRow row : mRows) {
+            updateVolumeRowH(row);
+        }
+        updateFooterH();
+    }
+
+    private void updateTextFooterH() {
+        final boolean zen = mState.zenMode != Global.ZEN_MODE_OFF;
+        final boolean wasVisible = mFooter.getVisibility() == View.VISIBLE;
+        Util.setVisOrGone(mTextFooter, mExpanded && mShowFooter && (zen || mShowing && wasVisible));
+        if (mTextFooter.getVisibility() == View.VISIBLE) {
+            String text = null;
+            String action = null;
+            if (mState.exitCondition != null) {
+                final long countdown = ZenModeConfig.tryParseCountdownConditionId(mState
+                        .exitCondition.id);
+                if (countdown != 0) {
+                    text = mContext.getString(R.string.volume_dnd_ends_at,
+                            Util.getShortTime(countdown));
+                    action = mContext.getString(R.string.volume_end_now);
+                } else {
+                    final DowntimeInfo info = ZenModeConfig.tryParseDowntimeConditionId(mState.
+                            exitCondition.id);
+                    if (info != null) {
+                        text = mContext.getString(R.string.volume_dnd_ends_at,
+                                Util.getShortTime(info));
+                        action = mContext.getString(R.string.volume_end_now);
+                    }
+                }
+            }
+            if (text == null) {
+                text = mContext.getString(R.string.volume_dnd_is_on);
+            }
+            if (action == null) {
+                action = mContext.getString(R.string.volume_turn_off);
+            }
+            Util.setText(mFootlineText, text);
+            Util.setText(mFootlineAction, action);
+            mFootlineAction.setOnClickListener(mTurnOffDnd);
+        }
+        Util.setVisOrGone(mFootlineText, zen);
+        Util.setVisOrGone(mFootlineAction, zen);
+    }
+
+    private void updateFooterH() {
+        if (!mShowFooter) {
+            Util.setVisOrGone(mFooter, false);
+            return;
+        }
+        if (mShowZenFooter) {
+            Util.setVisOrGone(mTextFooter, false);
+            final boolean ringActive = mActiveStream == AudioManager.STREAM_RING;
+            Util.setVisOrGone(mZenFooter, mZenFooter.isZen() && ringActive
+                    || mShowing && (mExpanded || mZenFooter.getVisibility() == View.VISIBLE));
+            mZenFooter.update();
+        } else {
+            Util.setVisOrGone(mZenFooter, false);
+            updateTextFooterH();
+        }
+    }
+
+    private void updateVolumeRowH(VolumeRow row) {
+        if (mState == null) return;
+        final StreamState ss = mState.states.get(row.stream);
+        if (ss == null) return;
+        row.ss = ss;
+        if (ss.level > 0) {
+            row.lastAudibleLevel = ss.level;
+        }
+        final boolean isRingStream = row.stream == AudioManager.STREAM_RING;
+        final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM;
+        final boolean isRingVibrate = isRingStream
+                && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE;
+        final boolean isNoned = (isRingStream || isSystemStream)
+                && mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
+        final boolean isLimited = isRingStream
+                && mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        final boolean isRingAndSuppressed = isRingStream && mState.effectsSuppressor != null;
+
+        // update slider max
+        final int max = ss.levelMax * 100;
+        if (max != row.slider.getMax()) {
+            row.slider.setMax(max);
+        }
+
+        // update header visible
+        if (row.cachedShowHeaders != mShowHeaders) {
+            row.cachedShowHeaders = mShowHeaders;
+            Util.setVisOrGone(row.header, mShowHeaders);
+        }
+
+        // update header text
+        final String text;
+        if (isRingAndSuppressed) {
+            text = mContext.getString(R.string.volume_stream_suppressed, ss.name,
+                    mState.effectsSuppressorName);
+        } else if (isNoned) {
+            text = mContext.getString(R.string.volume_stream_muted_dnd, ss.name);
+        } else if (isRingVibrate && isLimited) {
+            text = mContext.getString(R.string.volume_stream_vibrate_dnd, ss.name);
+        } else if (isRingVibrate) {
+            text = mContext.getString(R.string.volume_stream_vibrate, ss.name);
+        } else if (ss.muted || mAutomute && ss.level == 0) {
+            text = mContext.getString(R.string.volume_stream_muted, ss.name);
+        } else if (isLimited) {
+            text = mContext.getString(R.string.volume_stream_limited_dnd, ss.name);
+        } else {
+            text = ss.name;
+        }
+        Util.setText(row.header, text);
+
+        // update icon
+        final boolean iconEnabled = !isRingAndSuppressed && (mAutomute || ss.muteSupported);
+        row.icon.setEnabled(iconEnabled);
+        row.icon.setAlpha(iconEnabled ? 1 : 0.5f);
+        final int iconRes =
+                !isRingAndSuppressed && isRingVibrate ? R.drawable.ic_volume_ringer_vibrate
+                : ss.routedToBluetooth ?
+                        (ss.muted ? R.drawable.ic_volume_bt_mute : R.drawable.ic_volume_bt)
+                : isRingAndSuppressed || (mAutomute && ss.level == 0) ? row.iconMuteRes
+                : (ss.muted ? row.iconMuteRes : row.iconRes);
+        if (iconRes != row.cachedIconRes) {
+            if (row.cachedIconRes != 0 && isRingVibrate) {
+                mController.vibrate();
+            }
+            row.cachedIconRes = iconRes;
+            row.icon.setImageResource(iconRes);
+        }
+        row.iconState =
+                iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE
+                : (iconRes == R.drawable.ic_volume_bt_mute || iconRes == row.iconMuteRes)
+                        ? Events.ICON_STATE_MUTE
+                : (iconRes == R.drawable.ic_volume_bt || iconRes == row.iconRes)
+                        ? Events.ICON_STATE_UNMUTE
+                : Events.ICON_STATE_UNKNOWN;
+
+        // update slider
+        updateVolumeRowSliderH(row, isRingAndSuppressed);
+    }
+
+    private void updateVolumeRowSliderH(VolumeRow row, boolean isRingAndSuppressed) {
+        row.slider.setEnabled(!isRingAndSuppressed);
+        if (row.tracking) {
+            return;  // don't update if user is sliding
+        }
+        if (isRingAndSuppressed) {
+            row.slider.setProgress(0);
+            return;
+        }
+        final int progress = row.slider.getProgress();
+        final int level = getImpliedLevel(row.slider, progress);
+        final boolean rowVisible = row.view.getVisibility() == View.VISIBLE;
+        final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt)
+                < USER_ATTEMPT_GRACE_PERIOD;
+        mHandler.removeMessages(H.RECHECK, row);
+        if (mShowing && rowVisible && inGracePeriod) {
+            if (D.BUG) Log.d(TAG, "inGracePeriod");
+            mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row),
+                    row.userAttempt + USER_ATTEMPT_GRACE_PERIOD);
+            return;  // don't update if visible and in grace period
+        }
+        final int vlevel = row.ss.muted ? 0 : row.ss.level;
+        if (vlevel == level) {
+            if (mShowing && rowVisible) {
+                return;  // don't clamp if visible
+            }
+        }
+        final int newProgress = vlevel * 100;
+        if (progress != newProgress) {
+            if (mShowing && rowVisible) {
+                // animate!
+                if (row.anim != null && row.anim.isRunning()
+                        && row.animTargetProgress == newProgress) {
+                    return;  // already animating to the target progress
+                }
+                // start/update animation
+                if (row.anim == null) {
+                    row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress);
+                    row.anim.setInterpolator(new DecelerateInterpolator());
+                } else {
+                    row.anim.cancel();
+                    row.anim.setIntValues(progress, newProgress);
+                }
+                row.animTargetProgress = newProgress;
+                row.anim.setDuration(UPDATE_ANIMATION_DURATION);
+                row.anim.start();
+            } else {
+                // update slider directly to clamped value
+                if (row.anim != null) {
+                    row.anim.cancel();
+                }
+                row.slider.setProgress(newProgress);
+            }
+            if (mAutomute) {
+                if (vlevel == 0 && !row.ss.muted && row.stream == AudioManager.STREAM_MUSIC) {
+                    mController.setStreamMute(row.stream, true);
+                }
+            }
+        }
+    }
+
+    private void recheckH(VolumeRow row) {
+        if (row == null) {
+            if (D.BUG) Log.d(TAG, "recheckH ALL");
+            trimObsoleteH();
+            for (VolumeRow r : mRows) {
+                updateVolumeRowH(r);
+            }
+        } else {
+            if (D.BUG) Log.d(TAG, "recheckH " + row.stream);
+            updateVolumeRowH(row);
+        }
+    }
+
+    private void setStreamImportantH(int stream, boolean important) {
+        for (VolumeRow row : mRows) {
+            if (row.stream == stream) {
+                row.important = important;
+                return;
+            }
+        }
+    }
+
+    private final VolumeDialogController.Callbacks mControllerCallbackH
+            = new VolumeDialogController.Callbacks() {
+        @Override
+        public void onShowRequested(int reason) {
+            showH(reason);
+        }
+
+        @Override
+        public void onDismissRequested(int reason) {
+            dismissH(reason);
+        }
+
+        public void onScreenOff() {
+            dismissH(Events.DISMISS_REASON_SCREEN_OFF);
+        }
+
+        @Override
+        public void onStateChanged(State state) {
+            onStateChangedH(state);
+        }
+
+        @Override
+        public void onLayoutDirectionChanged(int layoutDirection) {
+            mDialogView.setLayoutDirection(layoutDirection);
+        }
+
+        @Override
+        public void onConfigurationChanged() {
+            updateWindowWidthH();
+            mSpTexts.update();
+        }
+
+        @Override
+        public void onShowVibrateHint() {
+            if (mSilentMode) {
+                mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false);
+            }
+        }
+
+        public void onShowSilentHint() {
+            if (mSilentMode) {
+                mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
+            }
+        }
+    };
+
+    private final OnClickListener mClickExpand = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mExpanding) return;
+            final boolean newExpand = !mExpanded;
+            Events.writeEvent(Events.EVENT_EXPAND, v);
+            setExpandedH(newExpand);
+        }
+    };
+
+    private final OnClickListener mClickSettings = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mSettingsButton.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
+                    onSettingsClickedH();
+                }
+            }, WAIT_FOR_RIPPLE);
+        }
+    };
+
+    private final View.OnClickListener mTurnOffDnd = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mSettingsButton.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    mController.setZenMode(Global.ZEN_MODE_OFF);
+                }
+            }, WAIT_FOR_RIPPLE);
+        }
+    };
+
+    private final ZenFooter.Callback mZenFooterCallback = new ZenFooter.Callback() {
+        @Override
+        public void onFooterExpanded() {
+            mHandler.sendEmptyMessage(H.RESCHEDULE_TIMEOUT);
+        }
+
+        @Override
+        public void onSettingsClicked() {
+            dismiss(Events.DISMISS_REASON_SETTINGS_CLICKED);
+            onZenSettingsClickedH();
+        }
+
+        @Override
+        public void onDoneClicked() {
+            dismiss(Events.DISMISS_REASON_DONE_CLICKED);
+        }
+    };
+
+    private final class H extends Handler {
+        private static final int SHOW = 1;
+        private static final int DISMISS = 2;
+        private static final int RECHECK = 3;
+        private static final int RECHECK_ALL = 4;
+        private static final int SET_STREAM_IMPORTANT = 5;
+        private static final int RESCHEDULE_TIMEOUT = 6;
+
+        public H() {
+            super(Looper.getMainLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case SHOW: showH(msg.arg1); break;
+                case DISMISS: dismissH(msg.arg1); break;
+                case RECHECK: recheckH((VolumeRow) msg.obj); break;
+                case RECHECK_ALL: recheckH(null); break;
+                case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break;
+                case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break;
+            }
+        }
+    }
+
+    private final class CustomDialog extends Dialog {
+        public CustomDialog(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean dispatchTouchEvent(MotionEvent ev) {
+            rescheduleTimeoutH();
+            return super.dispatchTouchEvent(ev);
+        }
+
+        @Override
+        protected void onStop() {
+            super.onStop();
+            mHandler.sendEmptyMessage(H.RECHECK_ALL);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            if (isShowing()) {
+                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+                    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
+        private final VolumeRow mRow;
+
+        private VolumeSeekBarChangeListener(VolumeRow row) {
+            mRow = row;
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            if (mRow.ss == null) return;
+            if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
+                    + " onProgressChanged " + progress + " fromUser=" + fromUser);
+            if (!fromUser) return;
+            if (mRow.ss.levelMin > 0) {
+                final int minProgress = mRow.ss.levelMin * 100;
+                if (progress < minProgress) {
+                    seekBar.setProgress(minProgress);
+                }
+            }
+            final int userLevel = getImpliedLevel(seekBar, progress);
+            if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) {
+                mRow.userAttempt = SystemClock.uptimeMillis();
+                if (mAutomute) {
+                    if (mRow.stream != AudioManager.STREAM_RING) {
+                        if (userLevel > 0 && mRow.ss.muted) {
+                            mController.setStreamMute(mRow.stream, false);
+                        }
+                        if (userLevel == 0 && mRow.ss.muteSupported && !mRow.ss.muted) {
+                            mController.setStreamMute(mRow.stream, true);
+                        }
+                    }
+                }
+                if (mRow.requestedLevel != userLevel) {
+                    mController.setStreamVolume(mRow.stream, userLevel);
+                    mRow.requestedLevel = userLevel;
+                    Events.writeEvent(Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, userLevel);
+                }
+            }
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream);
+            mController.setActiveStream(mRow.stream);
+            mRow.tracking = true;
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream);
+            mRow.tracking = false;
+            mRow.userAttempt = SystemClock.uptimeMillis();
+            int userLevel = getImpliedLevel(seekBar, seekBar.getProgress());
+            if (mRow.ss.level != userLevel) {
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow),
+                        USER_ATTEMPT_GRACE_PERIOD);
+            }
+        }
+    }
+
+    private static class VolumeRow {
+        private View view;
+        private View space;
+        private TextView header;
+        private ImageButton icon;
+        private SeekBar slider;
+        private ImageButton settingsButton;
+        private int stream;
+        private StreamState ss;
+        private long userAttempt;  // last user-driven slider change
+        private boolean tracking;  // tracking slider touch
+        private int requestedLevel;
+        private int iconRes;
+        private int iconMuteRes;
+        private boolean important;
+        private int cachedIconRes;
+        private int iconState;  // from Events
+        private boolean cachedShowHeaders = Prefs.DEFAULT_SHOW_HEADERS;
+        private int cachedExpandButtonRes;
+        private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
+        private int animTargetProgress;
+        private int lastAudibleLevel = 1;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
new file mode 100644
index 0000000..741e498
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.media.VolumePolicy;
+import android.os.Bundle;
+import android.os.Handler;
+
+import com.android.systemui.SystemUI;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Implementation of VolumeComponent backed by the new volume dialog.
+ */
+public class VolumeDialogComponent implements VolumeComponent {
+    private final SystemUI mSysui;
+    private final Context mContext;
+    private final VolumeDialogController mController;
+    private final ZenModeController mZenModeController;
+    private final VolumeDialog mDialog;
+
+    public VolumeDialogComponent(SystemUI sysui, Context context, Handler handler,
+            ZenModeController zen) {
+        mSysui = sysui;
+        mContext = context;
+        mController = new VolumeDialogController(context, null) {
+            @Override
+            protected void onUserActivityW() {
+                sendUserActivity();
+            }
+        };
+        mZenModeController = zen;
+        mDialog = new VolumeDialog(context, mController, zen) {
+            @Override
+            protected void onZenSettingsClickedH() {
+                startZenSettings();
+            }
+        };
+        applyConfiguration();
+    }
+
+    private void sendUserActivity() {
+        final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class);
+        if (kvm != null) {
+            kvm.userActivity();
+        }
+    }
+
+    private void applyConfiguration() {
+        mDialog.setStreamImportant(AudioManager.STREAM_ALARM, true);
+        mDialog.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
+        mDialog.setShowHeaders(false);
+        mDialog.setShowFooter(true);
+        mDialog.setZenFooter(true);
+        mDialog.setAutomute(true);
+        mDialog.setSilentMode(false);
+        mController.setVolumePolicy(VolumePolicy.DEFAULT);
+        mController.showDndTile(false);
+    }
+
+    @Override
+    public ZenModeController getZenController() {
+        return mZenModeController;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        // noop
+    }
+
+    @Override
+    public void dismissNow() {
+        mController.dismiss();
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        // noop
+    }
+
+    @Override
+    public void register() {
+        mController.register();
+        DndTile.setCombinedIcon(mContext, true);
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mController.dump(fd, pw, args);
+        mDialog.dump(pw);
+    }
+
+    private void startZenSettings() {
+        mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard(
+                ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java
new file mode 100644
index 0000000..ae5312e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogController.java
@@ -0,0 +1,962 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.IVolumeController;
+import android.media.VolumePolicy;
+import android.media.session.MediaController.PlaybackInfo;
+import android.media.session.MediaSession.Token;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.systemui.R;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ *  Source of truth for all state / events related to the volume dialog.  No presentation.
+ *
+ *  All work done on a dedicated background worker thread & associated worker.
+ *
+ *  Methods ending in "W" must be called on the worker thread.
+ */
+public class VolumeDialogController {
+    private static final String TAG = Util.logTag(VolumeDialogController.class);
+
+    private static final int DYNAMIC_STREAM_START_INDEX = 100;
+    private static final int VIBRATE_HINT_DURATION = 50;
+
+    private static final int[] STREAMS = {
+        AudioSystem.STREAM_ALARM,
+        AudioSystem.STREAM_BLUETOOTH_SCO,
+        AudioSystem.STREAM_DTMF,
+        AudioSystem.STREAM_MUSIC,
+        AudioSystem.STREAM_NOTIFICATION,
+        AudioSystem.STREAM_RING,
+        AudioSystem.STREAM_SYSTEM,
+        AudioSystem.STREAM_SYSTEM_ENFORCED,
+        AudioSystem.STREAM_TTS,
+        AudioSystem.STREAM_VOICE_CALL,
+    };
+
+    private final HandlerThread mWorkerThread;
+    private final W mWorker;
+    private final Context mContext;
+    private final AudioManager mAudio;
+    private final NotificationManager mNoMan;
+    private final ComponentName mComponent;
+    private final SettingObserver mObserver;
+    private final Receiver mReceiver = new Receiver();
+    private final MediaSessions mMediaSessions;
+    private final VC mVolumeController = new VC();
+    private final C mCallbacks = new C();
+    private final State mState = new State();
+    private final String[] mStreamTitles;
+    private final MediaSessionsCallbacks mMediaSessionsCallbacksW = new MediaSessionsCallbacks();
+    private final Vibrator mVibrator;
+    private final boolean mHasVibrator;
+
+    private boolean mEnabled;
+    private boolean mDestroyed;
+    private VolumePolicy mVolumePolicy = new VolumePolicy(true, true, false, 400);
+    private boolean mShowDndTile = false;
+
+    public VolumeDialogController(Context context, ComponentName component) {
+        mContext = context.getApplicationContext();
+        Events.writeEvent(Events.EVENT_COLLECTION_STARTED);
+        mComponent = component;
+        mWorkerThread = new HandlerThread(VolumeDialogController.class.getSimpleName());
+        mWorkerThread.start();
+        mWorker = new W(mWorkerThread.getLooper());
+        mMediaSessions = createMediaSessions(mContext, mWorkerThread.getLooper(),
+                mMediaSessionsCallbacksW);
+        mAudio = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mNoMan = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mObserver = new SettingObserver(mWorker);
+        mObserver.init();
+        mReceiver.init();
+        mStreamTitles = mContext.getResources().getStringArray(R.array.volume_stream_titles);
+        mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
+        mHasVibrator = mVibrator != null && mVibrator.hasVibrator();
+    }
+
+    public void dismiss() {
+        mCallbacks.onDismissRequested(Events.DISMISS_REASON_VOLUME_CONTROLLER);
+    }
+
+    public void register() {
+        try {
+            mAudio.setVolumeController(mVolumeController);
+        } catch (SecurityException e) {
+            Log.w(TAG, "Unable to set the volume controller", e);
+            return;
+        }
+        setVolumePolicy(mVolumePolicy);
+        showDndTile(mShowDndTile);
+        try {
+            mMediaSessions.init();
+        } catch (SecurityException e) {
+            Log.w(TAG, "No access to media sessions", e);
+        }
+    }
+
+    public void setVolumePolicy(VolumePolicy policy) {
+        mVolumePolicy = policy;
+        try {
+            mAudio.setVolumePolicy(mVolumePolicy);
+        } catch (NoSuchMethodError e) {
+            Log.w(TAG, "No volume policy api");
+        }
+    }
+
+    protected MediaSessions createMediaSessions(Context context, Looper looper,
+            MediaSessions.Callbacks callbacks) {
+        return new MediaSessions(context, looper, callbacks);
+    }
+
+    public void destroy() {
+        if (D.BUG) Log.d(TAG, "destroy");
+        if (mDestroyed) return;
+        mDestroyed = true;
+        Events.writeEvent(Events.EVENT_COLLECTION_STOPPED);
+        mMediaSessions.destroy();
+        mObserver.destroy();
+        mReceiver.destroy();
+        mWorkerThread.quitSafely();
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(VolumeDialogController.class.getSimpleName() + " state:");
+        pw.print("  mEnabled: "); pw.println(mEnabled);
+        pw.print("  mDestroyed: "); pw.println(mDestroyed);
+        pw.print("  mVolumePolicy: "); pw.println(mVolumePolicy);
+        pw.print("  mEnabled: "); pw.println(mEnabled);
+        pw.print("  mShowDndTile: "); pw.println(mShowDndTile);
+        pw.print("  mHasVibrator: "); pw.println(mHasVibrator);
+        pw.print("  mRemoteStreams: "); pw.println(mMediaSessionsCallbacksW.mRemoteStreams
+                .values());
+        pw.println();
+        mMediaSessions.dump(pw);
+    }
+
+    public void addCallback(Callbacks callback, Handler handler) {
+        mCallbacks.add(callback, handler);
+    }
+
+    public void removeCallback(Callbacks callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void getState() {
+        if (mDestroyed) return;
+        mWorker.sendEmptyMessage(W.GET_STATE);
+    }
+
+    public void notifyVisible(boolean visible) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
+    }
+
+    public void userActivity() {
+        if (mDestroyed) return;
+        mWorker.removeMessages(W.USER_ACTIVITY);
+        mWorker.sendEmptyMessage(W.USER_ACTIVITY);
+    }
+
+    public void setRingerMode(int value, boolean external) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_RINGER_MODE, value, external ? 1 : 0).sendToTarget();
+    }
+
+    public void setZenMode(int value) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_ZEN_MODE, value, 0).sendToTarget();
+    }
+
+    public void setExitCondition(Condition condition) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_EXIT_CONDITION, condition).sendToTarget();
+    }
+
+    public void setStreamMute(int stream, boolean mute) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_STREAM_MUTE, stream, mute ? 1 : 0).sendToTarget();
+    }
+
+    public void setStreamVolume(int stream, int level) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_STREAM_VOLUME, stream, level).sendToTarget();
+    }
+
+    public void setActiveStream(int stream) {
+        if (mDestroyed) return;
+        mWorker.obtainMessage(W.SET_ACTIVE_STREAM, stream, 0).sendToTarget();
+    }
+
+    public void vibrate() {
+        if (mHasVibrator) {
+            mVibrator.vibrate(VIBRATE_HINT_DURATION);
+        }
+    }
+
+    public boolean hasVibrator() {
+        return mHasVibrator;
+    }
+
+    private void onNotifyVisibleW(boolean visible) {
+        if (mDestroyed) return;
+        mAudio.notifyVolumeControllerVisible(mVolumeController, visible);
+        if (!visible) {
+            if (updateActiveStreamW(-1)) {
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+    }
+
+    protected void onUserActivityW() {
+        // hook for subclasses
+    }
+
+    private boolean checkRoutedToBluetoothW(int stream) {
+        boolean changed = false;
+        if (stream == AudioManager.STREAM_MUSIC) {
+            final boolean routedToBluetooth =
+                    (mAudio.getDevicesForStream(AudioManager.STREAM_MUSIC) &
+                            (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
+                            AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
+                            AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0;
+            changed |= updateStreamRoutedToBluetoothW(stream, routedToBluetooth);
+        }
+        return changed;
+    }
+
+    private void onVolumeChangedW(int stream, int flags) {
+        final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0;
+        final boolean fromKey = (flags & AudioManager.FLAG_FROM_KEY) != 0;
+        final boolean showVibrateHint = (flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0;
+        final boolean showSilentHint = (flags & AudioManager.FLAG_SHOW_SILENT_HINT) != 0;
+        boolean changed = false;
+        if (showUI) {
+            changed |= updateActiveStreamW(stream);
+        }
+        changed |= updateStreamLevelW(stream, mAudio.getLastAudibleStreamVolume(stream));
+        changed |= checkRoutedToBluetoothW(showUI ? AudioManager.STREAM_MUSIC : stream);
+        if (changed) {
+            mCallbacks.onStateChanged(mState);
+        }
+        if (showUI) {
+            mCallbacks.onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
+        }
+        if (showVibrateHint) {
+            mCallbacks.onShowVibrateHint();
+        }
+        if (showSilentHint) {
+            mCallbacks.onShowSilentHint();
+        }
+        if (changed && fromKey) {
+            Events.writeEvent(Events.EVENT_KEY);
+        }
+    }
+
+    private boolean updateActiveStreamW(int activeStream) {
+        if (activeStream == mState.activeStream) return false;
+        mState.activeStream = activeStream;
+        Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream);
+        if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream);
+        final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1;
+        if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s);
+        mAudio.forceVolumeControlStream(s);
+        return true;
+    }
+
+    private StreamState streamStateW(int stream) {
+        StreamState ss = mState.states.get(stream);
+        if (ss == null) {
+            ss = new StreamState();
+            mState.states.put(stream, ss);
+        }
+        return ss;
+    }
+
+    private void onGetStateW() {
+        for (int stream : STREAMS) {
+            updateStreamLevelW(stream, mAudio.getLastAudibleStreamVolume(stream));
+            streamStateW(stream).levelMin = mAudio.getStreamMinVolume(stream);
+            streamStateW(stream).levelMax = mAudio.getStreamMaxVolume(stream);
+            updateStreamMuteW(stream, mAudio.isStreamMute(stream));
+            final StreamState ss = streamStateW(stream);
+            ss.muteSupported = mAudio.isStreamAffectedByMute(stream);
+            ss.name = mStreamTitles[stream];
+            checkRoutedToBluetoothW(stream);
+        }
+        updateRingerModeExternalW(mAudio.getRingerMode());
+        updateZenModeW();
+        updateEffectsSuppressorW(mNoMan.getEffectsSuppressor());
+        updateExitConditionW();
+        mCallbacks.onStateChanged(mState);
+    }
+
+    private boolean updateStreamRoutedToBluetoothW(int stream, boolean routedToBluetooth) {
+        final StreamState ss = streamStateW(stream);
+        if (ss.routedToBluetooth == routedToBluetooth) return false;
+        ss.routedToBluetooth = routedToBluetooth;
+        if (D.BUG) Log.d(TAG, "updateStreamRoutedToBluetoothW stream=" + stream
+                + " routedToBluetooth=" + routedToBluetooth);
+        return true;
+    }
+
+    private boolean updateStreamLevelW(int stream, int level) {
+        final StreamState ss = streamStateW(stream);
+        if (ss.level == level) return false;
+        ss.level = level;
+        if (isLogWorthy(stream)) {
+            Events.writeEvent(Events.EVENT_LEVEL_CHANGED, stream, level);
+        }
+        return true;
+    }
+
+    private static boolean isLogWorthy(int stream) {
+        switch (stream) {
+            case AudioSystem.STREAM_ALARM:
+            case AudioSystem.STREAM_BLUETOOTH_SCO:
+            case AudioSystem.STREAM_MUSIC:
+            case AudioSystem.STREAM_RING:
+            case AudioSystem.STREAM_SYSTEM:
+            case AudioSystem.STREAM_VOICE_CALL:
+                return true;
+        }
+        return false;
+    }
+
+    private boolean updateStreamMuteW(int stream, boolean muted) {
+        final StreamState ss = streamStateW(stream);
+        if (ss.muted == muted) return false;
+        ss.muted = muted;
+        if (isLogWorthy(stream)) {
+            Events.writeEvent(Events.EVENT_MUTE_CHANGED, stream, muted);
+        }
+        if (muted && isRinger(stream)) {
+            updateRingerModeInternalW(mAudio.getRingerModeInternal());
+        }
+        return true;
+    }
+
+    private static boolean isRinger(int stream) {
+        return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
+    }
+
+    private boolean updateExitConditionW() {
+        final Condition exitCondition = mNoMan.getZenModeCondition();
+        if (Objects.equals(mState.exitCondition, exitCondition)) return false;
+        mState.exitCondition = exitCondition;
+        return true;
+    }
+
+    private boolean updateEffectsSuppressorW(ComponentName effectsSuppressor) {
+        if (Objects.equals(mState.effectsSuppressor, effectsSuppressor)) return false;
+        mState.effectsSuppressor = effectsSuppressor;
+        mState.effectsSuppressorName = getApplicationName(mContext, mState.effectsSuppressor);
+        Events.writeEvent(Events.EVENT_SUPPRESSOR_CHANGED, mState.effectsSuppressor,
+                mState.effectsSuppressorName);
+        return true;
+    }
+
+    private static String getApplicationName(Context context, ComponentName component) {
+        if (component == null) return null;
+        final PackageManager pm = context.getPackageManager();
+        final String pkg = component.getPackageName();
+        try {
+            final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
+            final String rt = Objects.toString(ai.loadLabel(pm), "").trim();
+            if (rt.length() > 0) {
+                return rt;
+            }
+        } catch (NameNotFoundException e) {}
+        return pkg;
+    }
+
+    private boolean updateZenModeW() {
+        final int zen = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
+        if (mState.zenMode == zen) return false;
+        mState.zenMode = zen;
+        Events.writeEvent(Events.EVENT_ZEN_MODE_CHANGED, zen);
+        return true;
+    }
+
+    private boolean updateRingerModeExternalW(int rm) {
+        if (rm == mState.ringerModeExternal) return false;
+        mState.ringerModeExternal = rm;
+        Events.writeEvent(Events.EVENT_EXTERNAL_RINGER_MODE_CHANGED, rm);
+        return true;
+    }
+
+    private boolean updateRingerModeInternalW(int rm) {
+        if (rm == mState.ringerModeInternal) return false;
+        mState.ringerModeInternal = rm;
+        Events.writeEvent(Events.EVENT_INTERNAL_RINGER_MODE_CHANGED, rm);
+        return true;
+    }
+
+    private void onSetRingerModeW(int mode, boolean external) {
+        if (external) {
+            mAudio.setRingerMode(mode);
+        } else {
+            mAudio.setRingerModeInternal(mode);
+        }
+    }
+
+    private void onSetStreamMuteW(int stream, boolean mute) {
+        mAudio.adjustStreamVolume(stream, mute ? AudioManager.ADJUST_MUTE
+                : AudioManager.ADJUST_UNMUTE, 0);
+    }
+
+    private void onSetStreamVolumeW(int stream, int level) {
+        if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level);
+        if (stream >= DYNAMIC_STREAM_START_INDEX) {
+            mMediaSessionsCallbacksW.setStreamVolume(stream, level);
+            return;
+        }
+        mAudio.setStreamVolume(stream, level, 0);
+    }
+
+    private void onSetActiveStreamW(int stream) {
+        boolean changed = updateActiveStreamW(stream);
+        if (changed) {
+            mCallbacks.onStateChanged(mState);
+        }
+    }
+
+    private void onSetExitConditionW(Condition condition) {
+        mNoMan.setZenModeCondition(condition);
+    }
+
+    private void onSetZenModeW(int mode) {
+        if (D.BUG) Log.d(TAG, "onSetZenModeW " + mode);
+        mNoMan.setZenMode(mode);
+    }
+
+    private void onDismissRequestedW(int reason) {
+        mCallbacks.onDismissRequested(reason);
+    }
+
+    public void showDndTile(boolean visible) {
+        if (D.BUG) Log.d(TAG, "showDndTile");
+        mContext.sendBroadcast(new Intent("com.android.systemui.dndtile.SET_VISIBLE")
+                .putExtra("visible", visible));
+    }
+
+    private final class VC extends IVolumeController.Stub {
+        private final String TAG = VolumeDialogController.TAG + ".VC";
+
+        @Override
+        public void displaySafeVolumeWarning(int flags) throws RemoteException {
+            // noop
+        }
+
+        @Override
+        public void volumeChanged(int streamType, int flags) throws RemoteException {
+            if (D.BUG) Log.d(TAG, "volumeChanged " + AudioSystem.streamToString(streamType)
+                    + " " + Util.audioManagerFlagsToString(flags));
+            if (mDestroyed) return;
+            mWorker.obtainMessage(W.VOLUME_CHANGED, streamType, flags).sendToTarget();
+        }
+
+        @Override
+        public void masterMuteChanged(int flags) throws RemoteException {
+            if (D.BUG) Log.d(TAG, "masterMuteChanged");
+        }
+
+        @Override
+        public void setLayoutDirection(int layoutDirection) throws RemoteException {
+            if (D.BUG) Log.d(TAG, "setLayoutDirection");
+            if (mDestroyed) return;
+            mWorker.obtainMessage(W.LAYOUT_DIRECTION_CHANGED, layoutDirection, 0).sendToTarget();
+        }
+
+        @Override
+        public void dismiss() throws RemoteException {
+            if (D.BUG) Log.d(TAG, "dismiss requested");
+            if (mDestroyed) return;
+            mWorker.obtainMessage(W.DISMISS_REQUESTED, Events.DISMISS_REASON_VOLUME_CONTROLLER, 0)
+                    .sendToTarget();
+            mWorker.sendEmptyMessage(W.DISMISS_REQUESTED);
+        }
+    }
+
+    private final class W extends Handler {
+        private static final int VOLUME_CHANGED = 1;
+        private static final int DISMISS_REQUESTED = 2;
+        private static final int GET_STATE = 3;
+        private static final int SET_RINGER_MODE = 4;
+        private static final int SET_ZEN_MODE = 5;
+        private static final int SET_EXIT_CONDITION = 6;
+        private static final int SET_STREAM_MUTE = 7;
+        private static final int LAYOUT_DIRECTION_CHANGED = 8;
+        private static final int CONFIGURATION_CHANGED = 9;
+        private static final int SET_STREAM_VOLUME = 10;
+        private static final int SET_ACTIVE_STREAM = 11;
+        private static final int NOTIFY_VISIBLE = 12;
+        private static final int USER_ACTIVITY = 13;
+
+        W(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case VOLUME_CHANGED: onVolumeChangedW(msg.arg1, msg.arg2); break;
+                case DISMISS_REQUESTED: onDismissRequestedW(msg.arg1); break;
+                case GET_STATE: onGetStateW(); break;
+                case SET_RINGER_MODE: onSetRingerModeW(msg.arg1, msg.arg2 != 0); break;
+                case SET_ZEN_MODE: onSetZenModeW(msg.arg1); break;
+                case SET_EXIT_CONDITION: onSetExitConditionW((Condition) msg.obj); break;
+                case SET_STREAM_MUTE: onSetStreamMuteW(msg.arg1, msg.arg2 != 0); break;
+                case LAYOUT_DIRECTION_CHANGED: mCallbacks.onLayoutDirectionChanged(msg.arg1); break;
+                case CONFIGURATION_CHANGED: mCallbacks.onConfigurationChanged(); break;
+                case SET_STREAM_VOLUME: onSetStreamVolumeW(msg.arg1, msg.arg2); break;
+                case SET_ACTIVE_STREAM: onSetActiveStreamW(msg.arg1); break;
+                case NOTIFY_VISIBLE: onNotifyVisibleW(msg.arg1 != 0);
+                case USER_ACTIVITY: onUserActivityW();
+            }
+        }
+    }
+
+    private final class C implements Callbacks {
+        private final HashMap<Callbacks, Handler> mCallbackMap = new HashMap<>();
+
+        public void add(Callbacks callback, Handler handler) {
+            if (callback == null || handler == null) throw new IllegalArgumentException();
+            mCallbackMap.put(callback, handler);
+        }
+
+        public void remove(Callbacks callback) {
+            mCallbackMap.remove(callback);
+        }
+
+        @Override
+        public void onShowRequested(final int reason) {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onShowRequested(reason);
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onDismissRequested(final int reason) {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onDismissRequested(reason);
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onStateChanged(final State state) {
+            final long time = System.currentTimeMillis();
+            final State copy = state.copy();
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onStateChanged(copy);
+                    }
+                });
+            }
+            Events.writeState(time, copy);
+        }
+
+        @Override
+        public void onLayoutDirectionChanged(final int layoutDirection) {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onLayoutDirectionChanged(layoutDirection);
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onConfigurationChanged() {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onConfigurationChanged();
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onShowVibrateHint() {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onShowVibrateHint();
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onShowSilentHint() {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onShowSilentHint();
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onScreenOff() {
+            for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+                entry.getValue().post(new Runnable() {
+                    @Override
+                    public void run() {
+                        entry.getKey().onScreenOff();
+                    }
+                });
+            }
+        }
+    }
+
+
+    private final class SettingObserver extends ContentObserver {
+        private final Uri SERVICE_URI = Settings.Secure.getUriFor(
+                Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT);
+        private final Uri ZEN_MODE_URI =
+                Settings.Global.getUriFor(Settings.Global.ZEN_MODE);
+        private final Uri ZEN_MODE_CONFIG_URI =
+                Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG);
+
+        public SettingObserver(Handler handler) {
+            super(handler);
+        }
+
+        public void init() {
+            mContext.getContentResolver().registerContentObserver(SERVICE_URI, false, this);
+            mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
+            mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this);
+            onChange(true, SERVICE_URI);
+        }
+
+        public void destroy() {
+            mContext.getContentResolver().unregisterContentObserver(this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            boolean changed = false;
+            if (SERVICE_URI.equals(uri)) {
+                final String setting = Settings.Secure.getString(mContext.getContentResolver(),
+                        Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT);
+                final boolean enabled = setting != null && mComponent != null
+                        && mComponent.equals(ComponentName.unflattenFromString(setting));
+                if (enabled == mEnabled) return;
+                if (enabled) {
+                    register();
+                }
+                mEnabled = enabled;
+            }
+            if (ZEN_MODE_URI.equals(uri)) {
+                changed = updateZenModeW();
+            }
+            if (ZEN_MODE_CONFIG_URI.equals(uri)) {
+                changed = updateExitConditionW();
+            }
+            if (changed) {
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+    }
+
+    private final class Receiver extends BroadcastReceiver {
+
+        public void init() {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
+            filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
+            filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+            filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+            filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+            filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+            filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+            filter.addAction(Intent.ACTION_SCREEN_OFF);
+            mContext.registerReceiver(this, filter, null, mWorker);
+        }
+
+        public void destroy() {
+            mContext.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            boolean changed = false;
+            if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
+                final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                final int level = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
+                final int oldLevel = intent
+                        .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1);
+                if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream
+                        + " level=" + level + " oldLevel=" + oldLevel);
+                changed = updateStreamLevelW(stream, level);
+            } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) {
+                final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                final int devices = intent
+                        .getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_DEVICES, -1);
+                final int oldDevices = intent
+                        .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_DEVICES, -1);
+                if (D.BUG) Log.d(TAG, "onReceive STREAM_DEVICES_CHANGED_ACTION stream="
+                        + stream + " devices=" + devices + " oldDevices=" + oldDevices);
+                changed = checkRoutedToBluetoothW(stream);
+            } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+                final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
+                if (D.BUG) Log.d(TAG, "onReceive RINGER_MODE_CHANGED_ACTION rm="
+                        + Util.ringerModeToString(rm));
+                changed = updateRingerModeExternalW(rm);
+            } else if (action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) {
+                final int rm = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
+                if (D.BUG) Log.d(TAG, "onReceive INTERNAL_RINGER_MODE_CHANGED_ACTION rm="
+                        + Util.ringerModeToString(rm));
+                changed = updateRingerModeInternalW(rm);
+            } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) {
+                final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                final boolean muted = intent
+                        .getBooleanExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+                if (D.BUG) Log.d(TAG, "onReceive STREAM_MUTE_CHANGED_ACTION stream=" + stream
+                        + " muted=" + muted);
+                changed = updateStreamMuteW(stream, muted);
+            } else if (action.equals(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED)) {
+                if (D.BUG) Log.d(TAG, "onReceive ACTION_EFFECTS_SUPPRESSOR_CHANGED");
+                changed = updateEffectsSuppressorW(mNoMan.getEffectsSuppressor());
+            } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+                if (D.BUG) Log.d(TAG, "onReceive ACTION_CONFIGURATION_CHANGED");
+                mCallbacks.onConfigurationChanged();
+            } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+                if (D.BUG) Log.d(TAG, "onReceive ACTION_SCREEN_OFF");
+                mCallbacks.onScreenOff();
+            }
+            if (changed) {
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+    }
+
+    private final class MediaSessionsCallbacks implements MediaSessions.Callbacks {
+        private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
+
+        private int mNextStream = DYNAMIC_STREAM_START_INDEX;
+
+        @Override
+        public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
+            if (!mRemoteStreams.containsKey(token)) {
+                mRemoteStreams.put(token, mNextStream);
+                if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + " is stream " + mNextStream);
+                mNextStream++;
+            }
+            final int stream = mRemoteStreams.get(token);
+            boolean changed = mState.states.indexOfKey(stream) < 0;
+            final StreamState ss = streamStateW(stream);
+            ss.dynamic = true;
+            ss.levelMin = 0;
+            ss.levelMax = pi.getMaxVolume();
+            if (ss.level != pi.getCurrentVolume()) {
+                ss.level = pi.getCurrentVolume();
+                changed = true;
+            }
+            if (!Objects.equals(ss.name, name)) {
+                ss.name = name;
+                changed = true;
+            }
+            if (changed) {
+                if (D.BUG) Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level
+                        + " of " + ss.levelMax);
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+
+        @Override
+        public void onRemoteVolumeChanged(Token token, int flags) {
+            final int stream = mRemoteStreams.get(token);
+            final boolean showUI = (flags & AudioManager.FLAG_SHOW_UI) != 0;
+            boolean changed = updateActiveStreamW(stream);
+            if (showUI) {
+                changed |= checkRoutedToBluetoothW(AudioManager.STREAM_MUSIC);
+            }
+            if (changed) {
+                mCallbacks.onStateChanged(mState);
+            }
+            if (showUI) {
+                mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
+            }
+        }
+
+        @Override
+        public void onRemoteRemoved(Token token) {
+            final int stream = mRemoteStreams.get(token);
+            mState.states.remove(stream);
+            if (mState.activeStream == stream) {
+                updateActiveStreamW(-1);
+            }
+            mCallbacks.onStateChanged(mState);
+        }
+
+        public void setStreamVolume(int stream, int level) {
+            final Token t = findToken(stream);
+            if (t == null) {
+                Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
+                return;
+            }
+            mMediaSessions.setVolume(t, level);
+        }
+
+        private Token findToken(int stream) {
+            for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) {
+                if (entry.getValue().equals(stream)) {
+                    return entry.getKey();
+                }
+            }
+            return null;
+        }
+    }
+
+    public static final class StreamState {
+        public boolean dynamic;
+        public int level;
+        public int levelMin;
+        public int levelMax;
+        public boolean muted;
+        public boolean muteSupported;
+        public String name;
+        public boolean routedToBluetooth;
+
+        public StreamState copy() {
+            final StreamState rt = new StreamState();
+            rt.dynamic = dynamic;
+            rt.level = level;
+            rt.levelMin = levelMin;
+            rt.levelMax = levelMax;
+            rt.muted = muted;
+            rt.muteSupported = muteSupported;
+            rt.name = name;
+            rt.routedToBluetooth = routedToBluetooth;
+            return rt;
+        }
+    }
+
+    public static final class State {
+        public static int NO_ACTIVE_STREAM = -1;
+
+        public final SparseArray<StreamState> states = new SparseArray<StreamState>();
+
+        public int ringerModeInternal;
+        public int ringerModeExternal;
+        public int zenMode;
+        public ComponentName effectsSuppressor;
+        public String effectsSuppressorName;
+        public Condition exitCondition;
+        public int activeStream = NO_ACTIVE_STREAM;
+
+        public State copy() {
+            final State rt = new State();
+            for (int i = 0; i < states.size(); i++) {
+                rt.states.put(states.keyAt(i), states.valueAt(i).copy());
+            }
+            rt.ringerModeExternal = ringerModeExternal;
+            rt.ringerModeInternal = ringerModeInternal;
+            rt.zenMode = zenMode;
+            if (effectsSuppressor != null) rt.effectsSuppressor = effectsSuppressor.clone();
+            rt.effectsSuppressorName = effectsSuppressorName;
+            if (exitCondition != null) rt.exitCondition = exitCondition.copy();
+            rt.activeStream = activeStream;
+            return rt;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder("{");
+            for (int i = 0; i < states.size(); i++) {
+                if (i > 0) sb.append(',');
+                final int stream = states.keyAt(i);
+                final StreamState ss = states.valueAt(i);
+                sb.append(AudioSystem.streamToString(stream)).append(":").append(ss.level)
+                        .append("[").append(ss.levelMin).append("..").append(ss.levelMax);
+                if (ss.muted) sb.append(" [MUTED]");
+            }
+            sb.append(",ringerModeExternal:").append(ringerModeExternal);
+            sb.append(",ringerModeInternal:").append(ringerModeInternal);
+            sb.append(",zenMode:").append(zenMode);
+            sb.append(",effectsSuppressor:").append(effectsSuppressor);
+            sb.append(",effectsSuppressorName:").append(effectsSuppressorName);
+            sb.append(",exitCondition:").append(exitCondition);
+            sb.append(",activeStream:").append(activeStream);
+            return sb.append('}').toString();
+        }
+    }
+
+    public interface Callbacks {
+        void onShowRequested(int reason);
+        void onDismissRequested(int reason);
+        void onStateChanged(State state);
+        void onLayoutDirectionChanged(int layoutDirection);
+        void onConfigurationChanged();
+        void onShowVibrateHint();
+        void onShowSilentHint();
+        void onScreenOff();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
index d16b818..50d5f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanel.java
@@ -384,7 +384,7 @@
         final Window window = mDialog.getWindow();
         window.requestFeature(Window.FEATURE_NO_TITLE);
         mDialog.setCanceledOnTouchOutside(true);
-        mDialog.setContentView(com.android.systemui.R.layout.volume_dialog);
+        mDialog.setContentView(com.android.systemui.R.layout.volume_panel_dialog);
         mDialog.setOnDismissListener(new OnDismissListener() {
             @Override
             public void onDismiss(DialogInterface dialog) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java
new file mode 100644
index 0000000..fa6ea9e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelComponent.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.media.IRemoteVolumeController;
+import android.media.IVolumeController;
+import android.media.VolumePolicy;
+import android.media.session.ISessionController;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Implementation of VolumeComponent backed by the old volume panel.
+ */
+public class VolumePanelComponent implements VolumeComponent {
+
+    private final SystemUI mSysui;
+    private final Context mContext;
+    private final Handler mHandler;
+    private final VolumeController mVolumeController;
+    private final RemoteVolumeController mRemoteVolumeController;
+    private final AudioManager mAudioManager;
+    private final MediaSessionManager mMediaSessionManager;
+
+    private VolumePanel mPanel;
+    private int mDismissDelay;
+
+    public VolumePanelComponent(SystemUI sysui, Context context, Handler handler,
+            ZenModeController controller) {
+        mSysui = sysui;
+        mContext = context;
+        mHandler = handler;
+        mAudioManager = context.getSystemService(AudioManager.class);
+        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
+        mVolumeController = new VolumeController();
+        mRemoteVolumeController = new RemoteVolumeController();
+        mDismissDelay = mContext.getResources().getInteger(R.integer.volume_panel_dismiss_delay);
+        mPanel = new VolumePanel(mContext, controller);
+        mPanel.setCallback(new VolumePanel.Callback() {
+            @Override
+            public void onZenSettings() {
+                mHandler.removeCallbacks(mStartZenSettings);
+                mHandler.post(mStartZenSettings);
+            }
+
+            @Override
+            public void onInteraction() {
+                final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class);
+                if (kvm != null) {
+                    kvm.userActivity();
+                }
+            }
+
+            @Override
+            public void onVisible(boolean visible) {
+                if (mAudioManager != null && mVolumeController != null) {
+                    mAudioManager.notifyVolumeControllerVisible(mVolumeController, visible);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (mPanel != null) {
+            mPanel.dump(fd, pw, args);
+        }
+    }
+
+    public void register() {
+        mAudioManager.setVolumeController(mVolumeController);
+        mAudioManager.setVolumePolicy(VolumePolicy.DEFAULT);
+        mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController);
+        DndTile.setVisible(mContext, false);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (mPanel != null) {
+            mPanel.onConfigurationChanged(newConfig);
+        }
+    }
+
+    @Override
+    public ZenModeController getZenController() {
+        return mPanel.getZenController();
+    }
+
+    @Override
+    public void dispatchDemoCommand(String command, Bundle args) {
+        mPanel.dispatchDemoCommand(command, args);
+    }
+
+    @Override
+    public void dismissNow() {
+        mPanel.postDismiss(0);
+    }
+
+    private final Runnable mStartZenSettings = new Runnable() {
+        @Override
+        public void run() {
+            mSysui.getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard(
+                    ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */);
+            mPanel.postDismiss(mDismissDelay);
+        }
+    };
+
+    private final class RemoteVolumeController extends IRemoteVolumeController.Stub {
+        @Override
+        public void remoteVolumeChanged(ISessionController binder, int flags)
+                throws RemoteException {
+            MediaController controller = new MediaController(mContext, binder);
+            mPanel.postRemoteVolumeChanged(controller, flags);
+        }
+
+        @Override
+        public void updateRemoteController(ISessionController session) throws RemoteException {
+            mPanel.postRemoteSliderVisibility(session != null);
+            // TODO stash default session in case the slider can be opened other
+            // than by remoteVolumeChanged.
+        }
+    }
+
+    /** For now, simply host an unmodified base volume panel in this process. */
+    private final class VolumeController extends IVolumeController.Stub {
+
+        @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 masterMuteChanged(int flags) throws RemoteException {
+            // no-op
+        }
+
+        @Override
+        public void setLayoutDirection(int layoutDirection)
+                throws RemoteException {
+            mPanel.postLayoutDirection(layoutDirection);
+        }
+
+        @Override
+        public void dismiss() throws RemoteException {
+            dismissNow();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
index ac08904..387aed0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java
@@ -29,25 +29,17 @@
 import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.media.AudioManager;
-import android.media.IRemoteVolumeController;
-import android.media.IVolumeController;
-import android.media.VolumePolicy;
-import android.media.session.ISessionController;
-import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
-import android.os.Bundle;
 import android.os.Handler;
-import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
-import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.qs.tiles.DndTile;
 import com.android.systemui.statusbar.ServiceMonitor;
-import com.android.systemui.statusbar.phone.PhoneStatusBar;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
@@ -59,6 +51,8 @@
     private static final String TAG = "VolumeUI";
     private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
 
+    private static final boolean USE_OLD_VOLUME = SystemProperties.getBoolean("volume.old", false);
+
     private final Handler mHandler = new Handler();
     private final Receiver mReceiver = new Receiver();
     private final RestorationNotification mRestorationNotification = new RestorationNotification();
@@ -67,12 +61,10 @@
     private AudioManager mAudioManager;
     private NotificationManager mNotificationManager;
     private MediaSessionManager mMediaSessionManager;
-    private VolumeController mVolumeController;
-    private RemoteVolumeController mRemoteVolumeController;
     private ServiceMonitor mVolumeControllerService;
 
-    private VolumePanel mPanel;
-    private int mDismissDelay;
+    private VolumePanelComponent mOldVolume;
+    private VolumeDialogComponent mNewVolume;
 
     @Override
     public void start() {
@@ -83,10 +75,10 @@
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
         mMediaSessionManager = (MediaSessionManager) mContext
                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
-        initPanel();
-        mVolumeController = new VolumeController();
-        mRemoteVolumeController = new RemoteVolumeController();
-        putComponent(VolumeComponent.class, mVolumeController);
+        final ZenModeController zenController = new ZenModeControllerImpl(mContext, mHandler);
+        mOldVolume = new VolumePanelComponent(this, mContext, mHandler, zenController);
+        mNewVolume = new VolumeDialogComponent(this, mContext, null, zenController);
+        putComponent(VolumeComponent.class, getVolumeComponent());
         mReceiver.start();
         mVolumeControllerService = new ServiceMonitor(TAG, LOGD,
                 mContext, Settings.Secure.VOLUME_CONTROLLER_SERVICE_COMPONENT,
@@ -94,30 +86,30 @@
         mVolumeControllerService.start();
     }
 
+    private VolumeComponent getVolumeComponent() {
+        return USE_OLD_VOLUME ? mOldVolume : mNewVolume;
+    }
+
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        if (mPanel != null) {
-            mPanel.onConfigurationChanged(newConfig);
-        }
+        if (!mEnabled) return;
+        getVolumeComponent().onConfigurationChanged(newConfig);
     }
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.print("mEnabled="); pw.println(mEnabled);
+        if (!mEnabled) return;
         pw.print("mVolumeControllerService="); pw.println(mVolumeControllerService.getComponent());
-        if (mPanel != null) {
-            mPanel.dump(fd, pw, args);
-        }
+        getVolumeComponent().dump(fd, pw, args);
     }
 
-    private void setVolumeController(boolean register) {
+    private void setDefaultVolumeController(boolean register) {
         if (register) {
-            if (LOGD) Log.d(TAG, "Registering default volume controller");
-            mAudioManager.setVolumeController(mVolumeController);
-            mAudioManager.setVolumePolicy(VolumePolicy.DEFAULT);
-            mMediaSessionManager.setRemoteVolumeController(mRemoteVolumeController);
             DndTile.setVisible(mContext, false);
+            if (LOGD) Log.d(TAG, "Registering default volume controller");
+            getVolumeComponent().register();
         } else {
             if (LOGD) Log.d(TAG, "Unregistering default volume controller");
             mAudioManager.setVolumeController(null);
@@ -125,33 +117,6 @@
         }
     }
 
-    private void initPanel() {
-        mDismissDelay = mContext.getResources().getInteger(R.integer.volume_panel_dismiss_delay);
-        mPanel = new VolumePanel(mContext, new ZenModeControllerImpl(mContext, mHandler));
-        mPanel.setCallback(new VolumePanel.Callback() {
-            @Override
-            public void onZenSettings() {
-                mHandler.removeCallbacks(mStartZenSettings);
-                mHandler.post(mStartZenSettings);
-            }
-
-            @Override
-            public void onInteraction() {
-                final KeyguardViewMediator kvm = getComponent(KeyguardViewMediator.class);
-                if (kvm != null) {
-                    kvm.userActivity();
-                }
-            }
-
-            @Override
-            public void onVisible(boolean visible) {
-                if (mAudioManager != null && mVolumeController != null) {
-                    mAudioManager.notifyVolumeControllerVisible(mVolumeController, visible);
-                }
-            }
-        });
-    }
-
     private String getAppLabel(ComponentName component) {
         final String pkg = component.getPackageName();
         try {
@@ -179,83 +144,11 @@
         d.show();
     }
 
-    private final Runnable mStartZenSettings = new Runnable() {
-        @Override
-        public void run() {
-            getComponent(PhoneStatusBar.class).startActivityDismissingKeyguard(
-                    ZenModePanel.ZEN_SETTINGS, true /* onlyProvisioned */, true /* dismissShade */);
-            mPanel.postDismiss(mDismissDelay);
-        }
-    };
-
-    /** For now, simply host an unmodified base volume panel in this process. */
-    private final class VolumeController extends IVolumeController.Stub implements VolumeComponent {
-
-        @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 masterMuteChanged(int flags) throws RemoteException {
-            // no-op
-        }
-
-        @Override
-        public void setLayoutDirection(int layoutDirection)
-                throws RemoteException {
-            mPanel.postLayoutDirection(layoutDirection);
-        }
-
-        @Override
-        public void dismiss() throws RemoteException {
-            dismissNow();
-        }
-
-        @Override
-        public ZenModeController getZenController() {
-            return mPanel.getZenController();
-        }
-
-        @Override
-        public void dispatchDemoCommand(String command, Bundle args) {
-            mPanel.dispatchDemoCommand(command, args);
-        }
-
-        @Override
-        public void dismissNow() {
-            mPanel.postDismiss(0);
-        }
-    }
-
-    private final class RemoteVolumeController extends IRemoteVolumeController.Stub {
-
-        @Override
-        public void remoteVolumeChanged(ISessionController binder, int flags)
-                throws RemoteException {
-            MediaController controller = new MediaController(mContext, binder);
-            mPanel.postRemoteVolumeChanged(controller, flags);
-        }
-
-        @Override
-        public void updateRemoteController(ISessionController session) throws RemoteException {
-            mPanel.postRemoteSliderVisibility(session != null);
-            // TODO stash default session in case the slider can be opened other
-            // than by remoteVolumeChanged.
-        }
-    }
-
     private final class ServiceMonitorCallbacks implements ServiceMonitor.Callbacks {
         @Override
         public void onNoService() {
             if (LOGD) Log.d(TAG, "onNoService");
-            setVolumeController(true);
+            setDefaultVolumeController(true);
             mRestorationNotification.hide();
             if (!mVolumeControllerService.isPackageAvailable()) {
                 mVolumeControllerService.setComponent(null);
@@ -267,8 +160,8 @@
             if (LOGD) Log.d(TAG, "onServiceStartAttempt");
             // poke the setting to update the uid
             mVolumeControllerService.setComponent(mVolumeControllerService.getComponent());
-            setVolumeController(false);
-            mVolumeController.dismissNow();
+            setDefaultVolumeController(false);
+            getVolumeComponent().dismissNow();
             mRestorationNotification.show();
             return 0;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
new file mode 100644
index 0000000..ba5b8d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import android.animation.LayoutTransition;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.Settings.Global;
+import android.service.notification.Condition;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+/**
+ * Switch bar + zen mode panel (conditions) attached to the bottom of the volume dialog.
+ */
+public class ZenFooter extends LinearLayout {
+    private static final String TAG = Util.logTag(ZenFooter.class);
+
+    private final Context mContext;
+    private final float mSecondaryAlpha;
+    private final LayoutTransition mLayoutTransition;
+
+    private ZenModeController mController;
+    private Switch mSwitch;
+    private ZenModePanel mZenModePanel;
+    private View mZenModePanelButtons;
+    private View mZenModePanelMoreButton;
+    private View mZenModePanelDoneButton;
+    private View mSwitchBar;
+    private View mSwitchBarIcon;
+    private View mSummary;
+    private TextView mSummaryLine1;
+    private TextView mSummaryLine2;
+    private boolean mFooterExpanded;
+    private int mZen = -1;
+    private Callback mCallback;
+
+    public ZenFooter(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        mSecondaryAlpha = getFloat(context.getResources(), R.dimen.volume_secondary_alpha);
+        mLayoutTransition = new LayoutTransition();
+        mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
+        mLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING);
+        mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+    }
+
+    private static float getFloat(Resources r, int resId) {
+        final TypedValue tv = new TypedValue();
+        r.getValue(resId, tv, true);
+        return tv.getFloat();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSwitchBar = findViewById(R.id.volume_zen_switch_bar);
+        mSwitchBarIcon = findViewById(R.id.volume_zen_switch_bar_icon);
+        mSwitch = (Switch) findViewById(R.id.volume_zen_switch);
+        mZenModePanel = (ZenModePanel) findViewById(R.id.zen_mode_panel);
+        mZenModePanelButtons = findViewById(R.id.volume_zen_mode_panel_buttons);
+        mZenModePanelMoreButton = findViewById(R.id.volume_zen_mode_panel_more);
+        mZenModePanelDoneButton = findViewById(R.id.volume_zen_mode_panel_done);
+        mSummary = findViewById(R.id.volume_zen_panel_summary);
+        mSummaryLine1 = (TextView) findViewById(R.id.volume_zen_panel_summary_line_1);
+        mSummaryLine2 = (TextView) findViewById(R.id.volume_zen_panel_summary_line_2);
+    }
+
+    public void init(ZenModeController controller, Callback callback) {
+        mCallback = callback;
+        mController = controller;
+        mZenModePanel.init(controller);
+        mZenModePanel.setEmbedded(true);
+        mSwitch.setOnCheckedChangeListener(mCheckedListener);
+        mController.addCallback(new ZenModeController.Callback() {
+            @Override
+            public void onZenChanged(int zen) {
+                setZen(zen);
+            }
+            @Override
+            public void onExitConditionChanged(Condition exitCondition) {
+                update();
+            }
+        });
+        mSwitchBar.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mSwitch.setChecked(!mSwitch.isChecked());
+            }
+        });
+        mZenModePanelMoreButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mCallback != null) {
+                    mCallback.onSettingsClicked();
+                }
+            }
+        });
+        mZenModePanelDoneButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mCallback != null) {
+                    mCallback.onDoneClicked();
+                }
+            }
+        });
+        mZen = mController.getZen();
+        update();
+    }
+
+    private void setZen(int zen) {
+        if (mZen == zen) return;
+        mZen = zen;
+        update();
+    }
+
+    public boolean isZen() {
+        return isZenPriority() || isZenNone();
+    }
+
+    private boolean isZenPriority() {
+        return mZen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+    }
+
+    private boolean isZenNone() {
+        return mZen == Global.ZEN_MODE_NO_INTERRUPTIONS;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        setLayoutTransition(null);
+        setFooterExpanded(false);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        setLayoutTransition(mLayoutTransition);
+    }
+
+    private boolean setFooterExpanded(boolean expanded) {
+        if (mFooterExpanded == expanded) return false;
+        mFooterExpanded = expanded;
+        update();
+        if (mCallback != null) {
+            mCallback.onFooterExpanded();
+        }
+        return true;
+    }
+
+    public boolean isFooterExpanded() {
+        return mFooterExpanded;
+    }
+
+    public void update() {
+        final boolean isZen = isZen();
+        mSwitch.setOnCheckedChangeListener(null);
+        mSwitch.setChecked(isZen);
+        mSwitch.setOnCheckedChangeListener(mCheckedListener);
+        Util.setVisOrGone(mZenModePanel, isZen && mFooterExpanded);
+        Util.setVisOrGone(mZenModePanelButtons, isZen && mFooterExpanded);
+        Util.setVisOrGone(mSummary, isZen && !mFooterExpanded);
+        mSwitchBarIcon.setAlpha(isZen ? 1 : mSecondaryAlpha);
+        final String line1 =
+                isZenPriority() ? mContext.getString(R.string.interruption_level_priority)
+                : isZenNone() ? mContext.getString(R.string.interruption_level_none)
+                : null;
+        Util.setText(mSummaryLine1, line1);
+        Util.setText(mSummaryLine2, mZenModePanel.getExitConditionText());
+    }
+
+    private final OnCheckedChangeListener mCheckedListener = new OnCheckedChangeListener() {
+        @Override
+        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+            if (D.BUG) Log.d(TAG, "onCheckedChanged " + isChecked);
+            if (isChecked != isZen()) {
+                final int newZen = isChecked ? Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+                        : Global.ZEN_MODE_OFF;
+                mZen = newZen;  // this one's optimistic
+                setFooterExpanded(isChecked);
+                mController.setZen(newZen);
+            }
+        }
+    };
+
+    public interface Callback {
+        void onFooterExpanded();
+        void onSettingsClicked();
+        void onDoneClicked();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
index 878ab712..a7f6175 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/ZenModePanel.java
@@ -23,7 +23,6 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
@@ -150,6 +149,7 @@
         if (mEmbedded == embedded) return;
         mEmbedded = embedded;
         mZenButtonsContainer.setLayoutTransition(mEmbedded ? null : newLayoutTransition(null));
+        setLayoutTransition(mEmbedded ? null : newLayoutTransition(null));
         if (mEmbedded) {
             mZenButtonsContainer.setBackground(null);
         } else {
@@ -166,12 +166,10 @@
         super.onFinishInflate();
 
         mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons);
-        mZenButtons.addButton(R.string.interruption_level_none, R.drawable.ic_zen_none,
-                Global.ZEN_MODE_NO_INTERRUPTIONS);
-        mZenButtons.addButton(R.string.interruption_level_priority, R.drawable.ic_zen_important,
+        mZenButtons.addButton(R.string.interruption_level_none, Global.ZEN_MODE_NO_INTERRUPTIONS);
+        mZenButtons.addButton(R.string.interruption_level_priority,
                 Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-        mZenButtons.addButton(R.string.interruption_level_all, R.drawable.ic_zen_all,
-                Global.ZEN_MODE_OFF);
+        mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF);
         mZenButtons.setCallback(mZenButtonsCallback);
 
         mZenButtonsContainer = (ViewGroup) findViewById(R.id.zen_buttons_container);
@@ -275,6 +273,7 @@
 
     private void setExpanded(boolean expanded) {
         if (expanded == mExpanded) return;
+        if (DEBUG) Log.d(mTag, "setExpanded " + expanded);
         mExpanded = expanded;
         if (mExpanded) {
             ensureSelection();
@@ -358,6 +357,10 @@
         return condition == null ? null : condition.copy();
     }
 
+    public String getExitConditionText() {
+        return mExitConditionText;
+    }
+
     private void refreshExitConditionText() {
         if (mExitCondition == null) {
             mExitConditionText = foreverSummary();
@@ -428,7 +431,7 @@
         mZenSubheadExpanded.setVisibility(expanded ? VISIBLE : GONE);
         mZenSubheadCollapsed.setVisibility(!expanded ? VISIBLE : GONE);
         mMoreSettings.setVisibility(zenImportant && expanded ? VISIBLE : GONE);
-        mZenConditions.setVisibility(!zenOff && expanded ? VISIBLE : GONE);
+        mZenConditions.setVisibility(mEmbedded || !zenOff && expanded ? VISIBLE : GONE);
 
         if (zenNone) {
             mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning);