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/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;
+    }
+}