1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.volume;
18
19import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
20import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
21
22import android.accessibilityservice.AccessibilityServiceInfo;
23import android.animation.ObjectAnimator;
24import android.annotation.NonNull;
25import android.annotation.SuppressLint;
26import android.app.Dialog;
27import android.app.KeyguardManager;
28import android.content.Context;
29import android.content.pm.PackageManager;
30import android.content.res.ColorStateList;
31import android.content.res.Configuration;
32import android.content.res.Resources;
33import android.graphics.Color;
34import android.graphics.PixelFormat;
35import android.graphics.Rect;
36import android.graphics.drawable.AnimatedVectorDrawable;
37import android.graphics.drawable.ColorDrawable;
38import android.graphics.drawable.Drawable;
39import android.media.AudioManager;
40import android.media.AudioSystem;
41import android.os.Debug;
42import android.os.Handler;
43import android.os.Looper;
44import android.os.Message;
45import android.os.SystemClock;
46import android.provider.Settings.Global;
47import android.transition.AutoTransition;
48import android.transition.Transition;
49import android.transition.TransitionManager;
50import android.util.DisplayMetrics;
51import android.util.Log;
52import android.util.Slog;
53import android.util.SparseBooleanArray;
54import android.view.Gravity;
55import android.view.MotionEvent;
56import android.view.View;
57import android.view.View.AccessibilityDelegate;
58import android.view.View.OnAttachStateChangeListener;
59import android.view.View.OnClickListener;
60import android.view.View.OnTouchListener;
61import android.view.ViewGroup;
62import android.view.ViewGroup.MarginLayoutParams;
63import android.view.Window;
64import android.view.WindowManager;
65import android.view.accessibility.AccessibilityEvent;
66import android.view.accessibility.AccessibilityManager;
67import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
68import android.view.animation.DecelerateInterpolator;
69import android.widget.ImageButton;
70import android.widget.SeekBar;
71import android.widget.SeekBar.OnSeekBarChangeListener;
72import android.widget.TextView;
73
74import com.android.settingslib.Utils;
75import com.android.systemui.Dependency;
76import com.android.systemui.Interpolators;
77import com.android.systemui.R;
78import com.android.systemui.plugins.VolumeDialogController;
79import com.android.systemui.plugins.VolumeDialogController.State;
80import com.android.systemui.plugins.VolumeDialogController.StreamState;
81import com.android.systemui.plugins.VolumeDialog;
82import com.android.systemui.statusbar.policy.ZenModeController;
83import com.android.systemui.tuner.TunerService;
84import com.android.systemui.tuner.TunerZenModePanel;
85
86import java.io.PrintWriter;
87import java.util.ArrayList;
88import java.util.List;
89
90/**
91 * Visual presentation of the volume dialog.
92 *
93 * A client of VolumeDialogControllerImpl and its state model.
94 *
95 * Methods ending in "H" must be called on the (ui) handler.
96 */
97public class VolumeDialogImpl implements VolumeDialog, TunerService.Tunable {
98    private static final String TAG = Util.logTag(VolumeDialogImpl.class);
99
100    public static final String SHOW_FULL_ZEN = "sysui_show_full_zen";
101
102    private static final long USER_ATTEMPT_GRACE_PERIOD = 1000;
103    private static final int UPDATE_ANIMATION_DURATION = 80;
104
105    private final Context mContext;
106    private final H mHandler = new H();
107    private VolumeDialogController mController;
108
109    private Window mWindow;
110    private CustomDialog mDialog;
111    private ViewGroup mDialogView;
112    private ViewGroup mDialogRowsView;
113    private ViewGroup mDialogContentView;
114    private ImageButton mExpandButton;
115    private final List<VolumeRow> mRows = new ArrayList<>();
116    private ConfigurableTexts mConfigurableTexts;
117    private final SparseBooleanArray mDynamic = new SparseBooleanArray();
118    private final KeyguardManager mKeyguard;
119    private final AudioManager mAudioManager;
120    private final AccessibilityManager mAccessibilityMgr;
121    private int mExpandButtonAnimationDuration;
122    private ZenFooter mZenFooter;
123    private final Object mSafetyWarningLock = new Object();
124    private final Accessibility mAccessibility = new Accessibility();
125    private final ColorStateList mActiveSliderTint;
126    private final ColorStateList mInactiveSliderTint;
127    private VolumeDialogMotion mMotion;
128    private int mWindowType;
129    private final ZenModeController mZenModeController;
130
131    private boolean mShowing;
132    private boolean mExpanded;
133    private boolean mShowA11yStream;
134
135    private int mActiveStream;
136    private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE;
137    private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
138    private State mState;
139    private boolean mExpandButtonAnimationRunning;
140    private SafetyWarningDialog mSafetyWarning;
141    private Callback mCallback;
142    private boolean mPendingStateChanged;
143    private boolean mPendingRecheckAll;
144    private long mCollapseTime;
145    private boolean mHovering = false;
146    private int mDensity;
147
148    private boolean mShowFullZen;
149    private TunerZenModePanel mZenPanel;
150
151    public VolumeDialogImpl(Context context) {
152        mContext = context;
153        mZenModeController = Dependency.get(ZenModeController.class);
154        mController = Dependency.get(VolumeDialogController.class);
155        mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
156        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
157        mAccessibilityMgr =
158                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
159        mActiveSliderTint = ColorStateList.valueOf(Utils.getColorAccent(mContext));
160        mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive);
161    }
162
163    public void init(int windowType, Callback callback) {
164        mCallback = callback;
165        mWindowType = windowType;
166
167        initDialog();
168
169        mAccessibility.init();
170
171        mController.addCallback(mControllerCallbackH, mHandler);
172        mController.getState();
173        Dependency.get(TunerService.class).addTunable(this, SHOW_FULL_ZEN);
174
175        final Configuration currentConfig = mContext.getResources().getConfiguration();
176        mDensity = currentConfig.densityDpi;
177    }
178
179    @Override
180    public void destroy() {
181        mController.removeCallback(mControllerCallbackH);
182    }
183
184    private void initDialog() {
185        mDialog = new CustomDialog(mContext);
186
187        mConfigurableTexts = new ConfigurableTexts(mContext);
188        mHovering = false;
189        mShowing = false;
190        mWindow = mDialog.getWindow();
191        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
192        mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
193        mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
194        mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
195                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
196                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
197                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
198                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
199                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
200        mDialog.setCanceledOnTouchOutside(true);
201        final Resources res = mContext.getResources();
202        final WindowManager.LayoutParams lp = mWindow.getAttributes();
203        lp.type = mWindowType;
204        lp.format = PixelFormat.TRANSLUCENT;
205        lp.setTitle(VolumeDialogImpl.class.getSimpleName());
206        lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
207        lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top);
208        lp.gravity = Gravity.TOP;
209        lp.windowAnimations = -1;
210        mWindow.setAttributes(lp);
211        mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
212
213        mDialog.setContentView(R.layout.volume_dialog);
214        mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog);
215        mDialogView.setOnHoverListener(new View.OnHoverListener() {
216            @Override
217            public boolean onHover(View v, MotionEvent event) {
218                int action = event.getActionMasked();
219                mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
220                        || (action == MotionEvent.ACTION_HOVER_MOVE);
221                rescheduleTimeoutH();
222                return true;
223            }
224        });
225        mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content);
226        mDialogRowsView = (ViewGroup) mDialogContentView.findViewById(R.id.volume_dialog_rows);
227        mExpanded = false;
228        mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button);
229        mExpandButton.setOnClickListener(mClickExpand);
230
231        mExpandButton.setVisibility(
232                AudioSystem.isSingleVolume(mContext) ? View.GONE : View.VISIBLE);
233        updateWindowWidthH();
234        updateExpandButtonH();
235
236        mMotion = new VolumeDialogMotion(mDialog, mDialogView, mDialogContentView, mExpandButton,
237                new VolumeDialogMotion.Callback() {
238                    @Override
239                    public void onAnimatingChanged(boolean animating) {
240                        if (animating) return;
241                        if (mPendingStateChanged) {
242                            mHandler.sendEmptyMessage(H.STATE_CHANGED);
243                            mPendingStateChanged = false;
244                        }
245                        if (mPendingRecheckAll) {
246                            mHandler.sendEmptyMessage(H.RECHECK_ALL);
247                            mPendingRecheckAll = false;
248                        }
249                    }
250                });
251
252        if (mRows.isEmpty()) {
253            addRow(AudioManager.STREAM_MUSIC,
254                    R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true);
255            if (!AudioSystem.isSingleVolume(mContext)) {
256                addRow(AudioManager.STREAM_RING,
257                        R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
258                addRow(AudioManager.STREAM_ALARM,
259                        R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false);
260                addRow(AudioManager.STREAM_VOICE_CALL,
261                        R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false);
262                addRow(AudioManager.STREAM_BLUETOOTH_SCO,
263                        R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false);
264                addRow(AudioManager.STREAM_SYSTEM,
265                        R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false);
266                addRow(AudioManager.STREAM_ACCESSIBILITY, R.drawable.ic_volume_accessibility,
267                        R.drawable.ic_volume_accessibility, true);
268            }
269        } else {
270            addExistingRows();
271        }
272        mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration);
273        mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer);
274        mZenFooter.init(mZenModeController);
275        mZenPanel = (TunerZenModePanel) mDialog.findViewById(R.id.tuner_zen_mode_panel);
276        mZenPanel.init(mZenModeController);
277        mZenPanel.setCallback(mZenPanelCallback);
278    }
279
280    @Override
281    public void onTuningChanged(String key, String newValue) {
282        if (SHOW_FULL_ZEN.equals(key)) {
283            mShowFullZen = newValue != null && Integer.parseInt(newValue) != 0;
284        }
285    }
286
287    private ColorStateList loadColorStateList(int colorResId) {
288        return ColorStateList.valueOf(mContext.getColor(colorResId));
289    }
290
291    private void updateWindowWidthH() {
292        final ViewGroup.LayoutParams lp = mDialogView.getLayoutParams();
293        final DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
294        if (D.BUG) Log.d(TAG, "updateWindowWidth dm.w=" + dm.widthPixels);
295        int w = dm.widthPixels;
296        final int max = mContext.getResources()
297                .getDimensionPixelSize(R.dimen.volume_dialog_panel_width);
298        if (w > max) {
299            w = max;
300        }
301        lp.width = w;
302        mDialogView.setLayoutParams(lp);
303    }
304
305    public void setStreamImportant(int stream, boolean important) {
306        mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget();
307    }
308
309    public void setAutomute(boolean automute) {
310        if (mAutomute == automute) return;
311        mAutomute = automute;
312        mHandler.sendEmptyMessage(H.RECHECK_ALL);
313    }
314
315    public void setSilentMode(boolean silentMode) {
316        if (mSilentMode == silentMode) return;
317        mSilentMode = silentMode;
318        mHandler.sendEmptyMessage(H.RECHECK_ALL);
319    }
320
321    private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) {
322        addRow(stream, iconRes, iconMuteRes, important, false);
323    }
324
325    private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
326            boolean dynamic) {
327        VolumeRow row = new VolumeRow();
328        initRow(row, stream, iconRes, iconMuteRes, important);
329        int rowSize;
330        int viewSize;
331        if (mShowA11yStream && dynamic && (rowSize = mRows.size()) > 1
332                && (viewSize = mDialogRowsView.getChildCount()) > 1) {
333            // A11y Stream should be the last in the list
334            mDialogRowsView.addView(row.view, viewSize - 2);
335            mRows.add(rowSize - 2, row);
336        } else {
337            mDialogRowsView.addView(row.view);
338            mRows.add(row);
339        }
340    }
341
342    private void addExistingRows() {
343        int N = mRows.size();
344        for (int i = 0; i < N; i++) {
345            final VolumeRow row = mRows.get(i);
346            initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important);
347            mDialogRowsView.addView(row.view);
348        }
349    }
350
351
352    private boolean isAttached() {
353        return mDialogContentView != null && mDialogContentView.isAttachedToWindow();
354    }
355
356    private VolumeRow getActiveRow() {
357        for (VolumeRow row : mRows) {
358            if (row.stream == mActiveStream) {
359                return row;
360            }
361        }
362        return mRows.get(0);
363    }
364
365    private VolumeRow findRow(int stream) {
366        for (VolumeRow row : mRows) {
367            if (row.stream == stream) return row;
368        }
369        return null;
370    }
371
372    public void dump(PrintWriter writer) {
373        writer.println(VolumeDialogImpl.class.getSimpleName() + " state:");
374        writer.print("  mShowing: "); writer.println(mShowing);
375        writer.print("  mExpanded: "); writer.println(mExpanded);
376        writer.print("  mExpandButtonAnimationRunning: ");
377        writer.println(mExpandButtonAnimationRunning);
378        writer.print("  mActiveStream: "); writer.println(mActiveStream);
379        writer.print("  mDynamic: "); writer.println(mDynamic);
380        writer.print("  mAutomute: "); writer.println(mAutomute);
381        writer.print("  mSilentMode: "); writer.println(mSilentMode);
382        writer.print("  mCollapseTime: "); writer.println(mCollapseTime);
383        writer.print("  mAccessibility.mFeedbackEnabled: ");
384        writer.println(mAccessibility.mFeedbackEnabled);
385    }
386
387    private static int getImpliedLevel(SeekBar seekBar, int progress) {
388        final int m = seekBar.getMax();
389        final int n = m / 100 - 1;
390        final int level = progress == 0 ? 0
391                : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n));
392        return level;
393    }
394
395    @SuppressLint("InflateParams")
396    private void initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes,
397            boolean important) {
398        row.stream = stream;
399        row.iconRes = iconRes;
400        row.iconMuteRes = iconMuteRes;
401        row.important = important;
402        row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null);
403        row.view.setId(row.stream);
404        row.view.setTag(row);
405        row.header = (TextView) row.view.findViewById(R.id.volume_row_header);
406        row.header.setId(20 * row.stream);
407        row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider);
408        row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
409        row.anim = null;
410
411        // forward events above the slider into the slider
412        row.view.setOnTouchListener(new OnTouchListener() {
413            private final Rect mSliderHitRect = new Rect();
414            private boolean mDragging;
415
416            @SuppressLint("ClickableViewAccessibility")
417            @Override
418            public boolean onTouch(View v, MotionEvent event) {
419                row.slider.getHitRect(mSliderHitRect);
420                if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN
421                        && event.getY() < mSliderHitRect.top) {
422                    mDragging = true;
423                }
424                if (mDragging) {
425                    event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top);
426                    row.slider.dispatchTouchEvent(event);
427                    if (event.getActionMasked() == MotionEvent.ACTION_UP
428                            || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
429                        mDragging = false;
430                    }
431                    return true;
432                }
433                return false;
434            }
435        });
436        row.icon = row.view.findViewById(R.id.volume_row_icon);
437        row.icon.setImageResource(iconRes);
438        if (row.stream != AudioSystem.STREAM_ACCESSIBILITY) {
439            row.icon.setOnClickListener(new OnClickListener() {
440                @Override
441                public void onClick(View v) {
442                    Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState);
443                    mController.setActiveStream(row.stream);
444                    if (row.stream == AudioManager.STREAM_RING) {
445                        final boolean hasVibrator = mController.hasVibrator();
446                        if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
447                            if (hasVibrator) {
448                                mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
449                            } else {
450                                final boolean wasZero = row.ss.level == 0;
451                                mController.setStreamVolume(stream,
452                                        wasZero ? row.lastAudibleLevel : 0);
453                            }
454                        } else {
455                            mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
456                            if (row.ss.level == 0) {
457                                mController.setStreamVolume(stream, 1);
458                            }
459                        }
460                    } else {
461                        final boolean vmute = row.ss.level == row.ss.levelMin;
462                        mController.setStreamVolume(stream,
463                                vmute ? row.lastAudibleLevel : row.ss.levelMin);
464                    }
465                    row.userAttempt = 0;  // reset the grace period, slider updates immediately
466                }
467            });
468        } else {
469            row.icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
470        }
471    }
472
473    public void show(int reason) {
474        mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget();
475    }
476
477    public void dismiss(int reason) {
478        mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
479    }
480
481    private void showH(int reason) {
482        if (D.BUG) Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
483        mHandler.removeMessages(H.SHOW);
484        mHandler.removeMessages(H.DISMISS);
485        rescheduleTimeoutH();
486        if (mShowing) return;
487        mShowing = true;
488        mMotion.startShow();
489        Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
490        mController.notifyVisible(true);
491    }
492
493    protected void rescheduleTimeoutH() {
494        mHandler.removeMessages(H.DISMISS);
495        final int timeout = computeTimeoutH();
496        mHandler.sendMessageDelayed(mHandler
497                .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout);
498        if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
499        mController.userActivity();
500    }
501
502    private int computeTimeoutH() {
503        if (mAccessibility.mFeedbackEnabled) return 20000;
504        if (mHovering) return 16000;
505        if (mSafetyWarning != null) return 5000;
506        if (mExpanded || mExpandButtonAnimationRunning) return 5000;
507        if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500;
508        return 3000;
509    }
510
511    protected void dismissH(int reason) {
512        if (mMotion.isAnimating()) {
513            return;
514        }
515        mHandler.removeMessages(H.DISMISS);
516        mHandler.removeMessages(H.SHOW);
517        if (!mShowing) return;
518        mShowing = false;
519        mMotion.startDismiss(new Runnable() {
520            @Override
521            public void run() {
522                updateExpandedH(false /* expanding */, true /* dismissing */);
523            }
524        });
525        if (mAccessibilityMgr.isEnabled()) {
526            AccessibilityEvent event =
527                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
528            event.setPackageName(mContext.getPackageName());
529            event.setClassName(CustomDialog.class.getSuperclass().getName());
530            event.getText().add(mContext.getString(
531                    R.string.volume_dialog_accessibility_dismissed_message));
532            mAccessibilityMgr.sendAccessibilityEvent(event);
533        }
534        Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason);
535        mController.notifyVisible(false);
536        synchronized (mSafetyWarningLock) {
537            if (mSafetyWarning != null) {
538                if (D.BUG) Log.d(TAG, "SafetyWarning dismissed");
539                mSafetyWarning.dismiss();
540            }
541        }
542    }
543
544    private void updateDialogBottomMarginH() {
545        final long diff = System.currentTimeMillis() - mCollapseTime;
546        final boolean collapsing = mCollapseTime != 0 && diff < getConservativeCollapseDuration();
547        final ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams();
548        final int bottomMargin = collapsing ? mDialogContentView.getHeight() :
549                mContext.getResources().getDimensionPixelSize(R.dimen.volume_dialog_margin_bottom);
550        if (bottomMargin != mlp.bottomMargin) {
551            if (D.BUG) Log.d(TAG, "bottomMargin " + mlp.bottomMargin + " -> " + bottomMargin);
552            mlp.bottomMargin = bottomMargin;
553            mDialogView.setLayoutParams(mlp);
554        }
555    }
556
557    private long getConservativeCollapseDuration() {
558        return mExpandButtonAnimationDuration * 3;
559    }
560
561    private void prepareForCollapse() {
562        mHandler.removeMessages(H.UPDATE_BOTTOM_MARGIN);
563        mCollapseTime = System.currentTimeMillis();
564        updateDialogBottomMarginH();
565        mHandler.sendEmptyMessageDelayed(H.UPDATE_BOTTOM_MARGIN, getConservativeCollapseDuration());
566    }
567
568    private void updateExpandedH(final boolean expanded, final boolean dismissing) {
569        if (mExpanded == expanded) return;
570        mExpanded = expanded;
571        mExpandButtonAnimationRunning = isAttached();
572        if (D.BUG) Log.d(TAG, "updateExpandedH " + expanded);
573        updateExpandButtonH();
574        updateFooterH();
575        TransitionManager.endTransitions(mDialogView);
576        final VolumeRow activeRow = getActiveRow();
577        if (!dismissing) {
578            mWindow.setLayout(mWindow.getAttributes().width, ViewGroup.LayoutParams.MATCH_PARENT);
579            TransitionManager.beginDelayedTransition(mDialogView, getTransistion());
580        }
581        updateRowsH(activeRow);
582        rescheduleTimeoutH();
583    }
584
585    private void updateExpandButtonH() {
586        if (D.BUG) Log.d(TAG, "updateExpandButtonH");
587        mExpandButton.setClickable(!mExpandButtonAnimationRunning);
588        if (!(mExpandButtonAnimationRunning && isAttached())) {
589            final int res = mExpanded ? R.drawable.ic_volume_collapse_animation
590                    : R.drawable.ic_volume_expand_animation;
591            if (hasTouchFeature()) {
592                mExpandButton.setImageResource(res);
593            } else {
594                // if there is no touch feature, show the volume ringer instead
595                mExpandButton.setImageResource(R.drawable.ic_volume_ringer);
596                mExpandButton.setBackgroundResource(0);  // remove gray background emphasis
597            }
598            mExpandButton.setContentDescription(mContext.getString(mExpanded ?
599                    R.string.accessibility_volume_collapse : R.string.accessibility_volume_expand));
600        }
601        if (mExpandButtonAnimationRunning) {
602            final Drawable d = mExpandButton.getDrawable();
603            if (d instanceof AnimatedVectorDrawable) {
604                // workaround to reset drawable
605                final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState()
606                        .newDrawable();
607                mExpandButton.setImageDrawable(avd);
608                avd.start();
609                mHandler.postDelayed(new Runnable() {
610                    @Override
611                    public void run() {
612                        mExpandButtonAnimationRunning = false;
613                        updateExpandButtonH();
614                        rescheduleTimeoutH();
615                    }
616                }, mExpandButtonAnimationDuration);
617            }
618        }
619    }
620
621    private boolean shouldBeVisibleH(VolumeRow row, boolean isActive) {
622        if (row.stream == AudioSystem.STREAM_ACCESSIBILITY) {
623            return mShowA11yStream;
624        }
625        return mExpanded && row.view.getVisibility() == View.VISIBLE
626                || (mExpanded && (row.important || isActive))
627                || !mExpanded && isActive;
628    }
629
630    private void updateRowsH(final VolumeRow activeRow) {
631        if (D.BUG) Log.d(TAG, "updateRowsH");
632        if (!mShowing) {
633            trimObsoleteH();
634        }
635        // apply changes to all rows
636        for (final VolumeRow row : mRows) {
637            final boolean isActive = row == activeRow;
638            final boolean shouldBeVisible = shouldBeVisibleH(row, isActive);
639            Util.setVisOrGone(row.view, shouldBeVisible);
640            Util.setVisOrGone(row.header, shouldBeVisible);
641            if (row.view.isShown()) {
642                updateVolumeRowSliderTintH(row, isActive);
643            }
644        }
645    }
646
647    private void trimObsoleteH() {
648        if (D.BUG) Log.d(TAG, "trimObsoleteH");
649        for (int i = mRows.size() - 1; i >= 0; i--) {
650            final VolumeRow row = mRows.get(i);
651            if (row.ss == null || !row.ss.dynamic) continue;
652            if (!mDynamic.get(row.stream)) {
653                mRows.remove(i);
654                mDialogRowsView.removeView(row.view);
655            }
656        }
657    }
658
659    private void onStateChangedH(State state) {
660        final boolean animating = mMotion.isAnimating();
661        if (D.BUG) Log.d(TAG, "onStateChangedH animating=" + animating);
662        mState = state;
663        if (animating) {
664            mPendingStateChanged = true;
665            return;
666        }
667        mDynamic.clear();
668        // add any new dynamic rows
669        for (int i = 0; i < state.states.size(); i++) {
670            final int stream = state.states.keyAt(i);
671            final StreamState ss = state.states.valueAt(i);
672            if (!ss.dynamic) continue;
673            mDynamic.put(stream, true);
674            if (findRow(stream) == null) {
675                addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true,
676                        true);
677            }
678        }
679
680        if (mActiveStream != state.activeStream) {
681            mActiveStream = state.activeStream;
682            updateRowsH(getActiveRow());
683            rescheduleTimeoutH();
684        }
685        for (VolumeRow row : mRows) {
686            updateVolumeRowH(row);
687        }
688        updateFooterH();
689    }
690
691    private void updateFooterH() {
692        if (D.BUG) Log.d(TAG, "updateFooterH");
693        final boolean wasVisible = mZenFooter.getVisibility() == View.VISIBLE;
694        final boolean visible = mState.zenMode != Global.ZEN_MODE_OFF
695                && (mAudioManager.isStreamAffectedByRingerMode(mActiveStream) || mExpanded)
696                && !mZenPanel.isEditing();
697
698        if (wasVisible != visible) {
699            mZenFooter.update();
700            Util.setVisOrGone(mZenFooter, visible);
701        }
702
703        final boolean fullWasVisible = mZenPanel.getVisibility() == View.VISIBLE;
704        final boolean fullVisible = mShowFullZen && !visible;
705        if (fullWasVisible != fullVisible) {
706            Util.setVisOrGone(mZenPanel, fullVisible);
707            if (fullVisible) {
708                mZenPanel.setZenState(mState.zenMode);
709                mZenPanel.setDoneListener(new OnClickListener() {
710                    @Override
711                    public void onClick(View v) {
712                        mHandler.sendEmptyMessage(H.UPDATE_FOOTER);
713                    }
714                });
715            }
716        }
717    }
718
719    private void updateVolumeRowH(VolumeRow row) {
720        if (D.BUG) Log.d(TAG, "updateVolumeRowH s=" + row.stream);
721        if (mState == null) return;
722        final StreamState ss = mState.states.get(row.stream);
723        if (ss == null) return;
724        row.ss = ss;
725        if (ss.level > 0) {
726            row.lastAudibleLevel = ss.level;
727        }
728        if (ss.level == row.requestedLevel) {
729            row.requestedLevel = -1;
730        }
731        final boolean isA11yStream = row.stream == AudioManager.STREAM_ACCESSIBILITY;
732        final boolean isRingStream = row.stream == AudioManager.STREAM_RING;
733        final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM;
734        final boolean isAlarmStream = row.stream == AudioManager.STREAM_ALARM;
735        final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC;
736        final boolean isRingVibrate = isRingStream
737                && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE;
738        final boolean isRingSilent = isRingStream
739                && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT;
740        final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS;
741        final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
742        final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream)
743                : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream)
744                : false;
745
746        // update slider max
747        final int max = ss.levelMax * 100;
748        if (max != row.slider.getMax()) {
749            row.slider.setMax(max);
750        }
751
752        // update header text
753        Util.setText(row.header, getStreamLabelH(ss));
754        mConfigurableTexts.add(row.header, ss.name);
755
756        // update icon
757        final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted;
758        row.icon.setEnabled(iconEnabled);
759        row.icon.setAlpha(iconEnabled ? 1 : 0.5f);
760        final int iconRes =
761                isRingVibrate ? R.drawable.ic_volume_ringer_vibrate
762                : isRingSilent || zenMuted ? row.cachedIconRes
763                : ss.routedToBluetooth ?
764                        (ss.muted ? R.drawable.ic_volume_media_bt_mute
765                                : R.drawable.ic_volume_media_bt)
766                : mAutomute && ss.level == 0 ? row.iconMuteRes
767                : (ss.muted ? row.iconMuteRes : row.iconRes);
768        if (iconRes != row.cachedIconRes) {
769            if (row.cachedIconRes != 0 && isRingVibrate) {
770                mController.vibrate();
771            }
772            row.cachedIconRes = iconRes;
773            row.icon.setImageResource(iconRes);
774        }
775        row.iconState =
776                iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE
777                : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes)
778                        ? Events.ICON_STATE_MUTE
779                : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes)
780                        ? Events.ICON_STATE_UNMUTE
781                : Events.ICON_STATE_UNKNOWN;
782        if (iconEnabled) {
783            if (isRingStream) {
784                if (isRingVibrate) {
785                    row.icon.setContentDescription(mContext.getString(
786                            R.string.volume_stream_content_description_unmute,
787                            getStreamLabelH(ss)));
788                } else {
789                    if (mController.hasVibrator()) {
790                        row.icon.setContentDescription(mContext.getString(
791                                mShowA11yStream
792                                        ? R.string.volume_stream_content_description_vibrate_a11y
793                                        : R.string.volume_stream_content_description_vibrate,
794                                getStreamLabelH(ss)));
795                    } else {
796                        row.icon.setContentDescription(mContext.getString(
797                                mShowA11yStream
798                                        ? R.string.volume_stream_content_description_mute_a11y
799                                        : R.string.volume_stream_content_description_mute,
800                                getStreamLabelH(ss)));
801                    }
802                }
803            } else if (isA11yStream) {
804                row.icon.setContentDescription(getStreamLabelH(ss));
805            } else {
806                if (ss.muted || mAutomute && ss.level == 0) {
807                   row.icon.setContentDescription(mContext.getString(
808                           R.string.volume_stream_content_description_unmute,
809                           getStreamLabelH(ss)));
810                } else {
811                    row.icon.setContentDescription(mContext.getString(
812                            mShowA11yStream
813                                    ? R.string.volume_stream_content_description_mute_a11y
814                                    : R.string.volume_stream_content_description_mute,
815                            getStreamLabelH(ss)));
816                }
817            }
818        } else {
819            row.icon.setContentDescription(getStreamLabelH(ss));
820        }
821
822        // update slider
823        final boolean enableSlider = !zenMuted;
824        final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0
825                : row.ss.level;
826        updateVolumeRowSliderH(row, enableSlider, vlevel);
827    }
828
829    private void updateVolumeRowSliderTintH(VolumeRow row, boolean isActive) {
830        if (isActive && mExpanded) {
831            row.slider.requestFocus();
832        }
833        final ColorStateList tint = isActive && row.slider.isEnabled() ? mActiveSliderTint
834                : mInactiveSliderTint;
835        if (tint == row.cachedSliderTint) return;
836        row.cachedSliderTint = tint;
837        row.slider.setProgressTintList(tint);
838        row.slider.setThumbTintList(tint);
839    }
840
841    private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) {
842        row.slider.setEnabled(enable);
843        updateVolumeRowSliderTintH(row, row.stream == mActiveStream);
844        if (row.tracking) {
845            return;  // don't update if user is sliding
846        }
847        final int progress = row.slider.getProgress();
848        final int level = getImpliedLevel(row.slider, progress);
849        final boolean rowVisible = row.view.getVisibility() == View.VISIBLE;
850        final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt)
851                < USER_ATTEMPT_GRACE_PERIOD;
852        mHandler.removeMessages(H.RECHECK, row);
853        if (mShowing && rowVisible && inGracePeriod) {
854            if (D.BUG) Log.d(TAG, "inGracePeriod");
855            mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row),
856                    row.userAttempt + USER_ATTEMPT_GRACE_PERIOD);
857            return;  // don't update if visible and in grace period
858        }
859        if (vlevel == level) {
860            if (mShowing && rowVisible) {
861                return;  // don't clamp if visible
862            }
863        }
864        final int newProgress = vlevel * 100;
865        if (progress != newProgress) {
866            if (mShowing && rowVisible) {
867                // animate!
868                if (row.anim != null && row.anim.isRunning()
869                        && row.animTargetProgress == newProgress) {
870                    return;  // already animating to the target progress
871                }
872                // start/update animation
873                if (row.anim == null) {
874                    row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress);
875                    row.anim.setInterpolator(new DecelerateInterpolator());
876                } else {
877                    row.anim.cancel();
878                    row.anim.setIntValues(progress, newProgress);
879                }
880                row.animTargetProgress = newProgress;
881                row.anim.setDuration(UPDATE_ANIMATION_DURATION);
882                row.anim.start();
883            } else {
884                // update slider directly to clamped value
885                if (row.anim != null) {
886                    row.anim.cancel();
887                }
888                row.slider.setProgress(newProgress, true);
889            }
890        }
891    }
892
893    private void recheckH(VolumeRow row) {
894        if (row == null) {
895            if (D.BUG) Log.d(TAG, "recheckH ALL");
896            trimObsoleteH();
897            for (VolumeRow r : mRows) {
898                updateVolumeRowH(r);
899            }
900        } else {
901            if (D.BUG) Log.d(TAG, "recheckH " + row.stream);
902            updateVolumeRowH(row);
903        }
904    }
905
906    private void setStreamImportantH(int stream, boolean important) {
907        for (VolumeRow row : mRows) {
908            if (row.stream == stream) {
909                row.important = important;
910                return;
911            }
912        }
913    }
914
915    private void showSafetyWarningH(int flags) {
916        if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0
917                || mShowing) {
918            synchronized (mSafetyWarningLock) {
919                if (mSafetyWarning != null) {
920                    return;
921                }
922                mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) {
923                    @Override
924                    protected void cleanUp() {
925                        synchronized (mSafetyWarningLock) {
926                            mSafetyWarning = null;
927                        }
928                        recheckH(null);
929                    }
930                };
931                mSafetyWarning.show();
932            }
933            recheckH(null);
934        }
935        rescheduleTimeoutH();
936    }
937
938    private String getStreamLabelH(StreamState ss) {
939        if (ss.remoteLabel != null) {
940            return ss.remoteLabel;
941        }
942        try {
943            return mContext.getString(ss.name);
944        } catch (Resources.NotFoundException e) {
945            Slog.e(TAG, "Can't find translation for stream " + ss);
946            return "";
947        }
948    }
949
950    private AutoTransition getTransistion() {
951        AutoTransition transition = new AutoTransition();
952        transition.setDuration(mExpandButtonAnimationDuration);
953        transition.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
954        transition.addListener(new Transition.TransitionListener() {
955            @Override
956            public void onTransitionStart(Transition transition) {
957            }
958
959            @Override
960            public void onTransitionEnd(Transition transition) {
961                mWindow.setLayout(
962                        mWindow.getAttributes().width, ViewGroup.LayoutParams.WRAP_CONTENT);
963            }
964
965            @Override
966            public void onTransitionCancel(Transition transition) {
967            }
968
969            @Override
970            public void onTransitionPause(Transition transition) {
971                mWindow.setLayout(
972                        mWindow.getAttributes().width, ViewGroup.LayoutParams.WRAP_CONTENT);
973            }
974
975            @Override
976            public void onTransitionResume(Transition transition) {
977            }
978        });
979        return transition;
980    }
981
982    private boolean hasTouchFeature() {
983        final PackageManager pm = mContext.getPackageManager();
984        return pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
985    }
986
987    private final VolumeDialogController.Callbacks mControllerCallbackH
988            = new VolumeDialogController.Callbacks() {
989        @Override
990        public void onShowRequested(int reason) {
991            showH(reason);
992        }
993
994        @Override
995        public void onDismissRequested(int reason) {
996            dismissH(reason);
997        }
998
999        @Override
1000        public void onScreenOff() {
1001            dismissH(Events.DISMISS_REASON_SCREEN_OFF);
1002        }
1003
1004        @Override
1005        public void onStateChanged(State state) {
1006            onStateChangedH(state);
1007        }
1008
1009        @Override
1010        public void onLayoutDirectionChanged(int layoutDirection) {
1011            mDialogView.setLayoutDirection(layoutDirection);
1012        }
1013
1014        @Override
1015        public void onConfigurationChanged() {
1016            Configuration newConfig = mContext.getResources().getConfiguration();
1017            final int density = newConfig.densityDpi;
1018            if (density != mDensity) {
1019                mDialog.dismiss();
1020                mZenFooter.cleanup();
1021                initDialog();
1022                mDensity = density;
1023            }
1024            updateWindowWidthH();
1025            mConfigurableTexts.update();
1026            mZenFooter.onConfigurationChanged();
1027        }
1028
1029        @Override
1030        public void onShowVibrateHint() {
1031            if (mSilentMode) {
1032                mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false);
1033            }
1034        }
1035
1036        @Override
1037        public void onShowSilentHint() {
1038            if (mSilentMode) {
1039                mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
1040            }
1041        }
1042
1043        @Override
1044        public void onShowSafetyWarning(int flags) {
1045            showSafetyWarningH(flags);
1046        }
1047
1048        @Override
1049        public void onAccessibilityModeChanged(Boolean showA11yStream) {
1050            boolean show = showA11yStream == null ? false : showA11yStream;
1051            mShowA11yStream = show;
1052            updateRowsH(getActiveRow());
1053
1054        }
1055    };
1056
1057    private final ZenModePanel.Callback mZenPanelCallback = new ZenModePanel.Callback() {
1058        @Override
1059        public void onPrioritySettings() {
1060            mCallback.onZenPrioritySettingsClicked();
1061        }
1062
1063        @Override
1064        public void onInteraction() {
1065            mHandler.sendEmptyMessage(H.RESCHEDULE_TIMEOUT);
1066        }
1067
1068        @Override
1069        public void onExpanded(boolean expanded) {
1070            // noop.
1071        }
1072    };
1073
1074    private final OnClickListener mClickExpand = new OnClickListener() {
1075        @Override
1076        public void onClick(View v) {
1077            if (mExpandButtonAnimationRunning) return;
1078            final boolean newExpand = !mExpanded;
1079            Events.writeEvent(mContext, Events.EVENT_EXPAND, newExpand);
1080            updateExpandedH(newExpand, false /* dismissing */);
1081        }
1082    };
1083
1084    private final class H extends Handler {
1085        private static final int SHOW = 1;
1086        private static final int DISMISS = 2;
1087        private static final int RECHECK = 3;
1088        private static final int RECHECK_ALL = 4;
1089        private static final int SET_STREAM_IMPORTANT = 5;
1090        private static final int RESCHEDULE_TIMEOUT = 6;
1091        private static final int STATE_CHANGED = 7;
1092        private static final int UPDATE_BOTTOM_MARGIN = 8;
1093        private static final int UPDATE_FOOTER = 9;
1094
1095        public H() {
1096            super(Looper.getMainLooper());
1097        }
1098
1099        @Override
1100        public void handleMessage(Message msg) {
1101            switch (msg.what) {
1102                case SHOW: showH(msg.arg1); break;
1103                case DISMISS: dismissH(msg.arg1); break;
1104                case RECHECK: recheckH((VolumeRow) msg.obj); break;
1105                case RECHECK_ALL: recheckH(null); break;
1106                case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break;
1107                case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break;
1108                case STATE_CHANGED: onStateChangedH(mState); break;
1109                case UPDATE_BOTTOM_MARGIN: updateDialogBottomMarginH(); break;
1110                case UPDATE_FOOTER: updateFooterH(); break;
1111            }
1112        }
1113    }
1114
1115    private final class CustomDialog extends Dialog {
1116        public CustomDialog(Context context) {
1117            super(context);
1118        }
1119
1120        @Override
1121        public boolean dispatchTouchEvent(MotionEvent ev) {
1122            rescheduleTimeoutH();
1123            return super.dispatchTouchEvent(ev);
1124        }
1125
1126        @Override
1127        protected void onStop() {
1128            super.onStop();
1129            final boolean animating = mMotion.isAnimating();
1130            if (D.BUG) Log.d(TAG, "onStop animating=" + animating);
1131            if (animating) {
1132                mPendingRecheckAll = true;
1133                return;
1134            }
1135            mHandler.sendEmptyMessage(H.RECHECK_ALL);
1136        }
1137
1138        @Override
1139        public boolean onTouchEvent(MotionEvent event) {
1140            if (isShowing()) {
1141                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
1142                    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
1143                    return true;
1144                }
1145            }
1146            return false;
1147        }
1148
1149        @Override
1150        public boolean dispatchPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) {
1151            event.setClassName(getClass().getSuperclass().getName());
1152            event.setPackageName(mContext.getPackageName());
1153
1154            ViewGroup.LayoutParams params = getWindow().getAttributes();
1155            boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) &&
1156                    (params.height == ViewGroup.LayoutParams.MATCH_PARENT);
1157            event.setFullScreen(isFullScreen);
1158
1159            if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
1160                if (mShowing) {
1161                    event.getText().add(mContext.getString(
1162                            R.string.volume_dialog_accessibility_shown_message,
1163                            getStreamLabelH(getActiveRow().ss)));
1164                    return true;
1165                }
1166            }
1167            return false;
1168        }
1169    }
1170
1171    private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
1172        private final VolumeRow mRow;
1173
1174        private VolumeSeekBarChangeListener(VolumeRow row) {
1175            mRow = row;
1176        }
1177
1178        @Override
1179        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
1180            if (mRow.ss == null) return;
1181            if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
1182                    + " onProgressChanged " + progress + " fromUser=" + fromUser);
1183            if (!fromUser) return;
1184            if (mRow.ss.levelMin > 0) {
1185                final int minProgress = mRow.ss.levelMin * 100;
1186                if (progress < minProgress) {
1187                    seekBar.setProgress(minProgress);
1188                    progress = minProgress;
1189                }
1190            }
1191            final int userLevel = getImpliedLevel(seekBar, progress);
1192            if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) {
1193                mRow.userAttempt = SystemClock.uptimeMillis();
1194                if (mRow.requestedLevel != userLevel) {
1195                    mController.setStreamVolume(mRow.stream, userLevel);
1196                    mRow.requestedLevel = userLevel;
1197                    Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream,
1198                            userLevel);
1199                }
1200            }
1201        }
1202
1203        @Override
1204        public void onStartTrackingTouch(SeekBar seekBar) {
1205            if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream);
1206            mController.setActiveStream(mRow.stream);
1207            mRow.tracking = true;
1208        }
1209
1210        @Override
1211        public void onStopTrackingTouch(SeekBar seekBar) {
1212            if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream);
1213            mRow.tracking = false;
1214            mRow.userAttempt = SystemClock.uptimeMillis();
1215            final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress());
1216            Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel);
1217            if (mRow.ss.level != userLevel) {
1218                mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow),
1219                        USER_ATTEMPT_GRACE_PERIOD);
1220            }
1221        }
1222    }
1223
1224    private final class Accessibility extends AccessibilityDelegate {
1225        private boolean mFeedbackEnabled;
1226
1227        public void init() {
1228            mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
1229                @Override
1230                public void onViewDetachedFromWindow(View v) {
1231                    if (D.BUG) Log.d(TAG, "onViewDetachedFromWindow");
1232                }
1233
1234                @Override
1235                public void onViewAttachedToWindow(View v) {
1236                    if (D.BUG) Log.d(TAG, "onViewAttachedToWindow");
1237                    updateFeedbackEnabled();
1238                }
1239            });
1240            mDialogView.setAccessibilityDelegate(this);
1241            mAccessibilityMgr.addAccessibilityStateChangeListener(
1242                    new AccessibilityStateChangeListener() {
1243                        @Override
1244                        public void onAccessibilityStateChanged(boolean enabled) {
1245                            updateFeedbackEnabled();
1246                        }
1247                    });
1248            updateFeedbackEnabled();
1249        }
1250
1251        @Override
1252        public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
1253                AccessibilityEvent event) {
1254            rescheduleTimeoutH();
1255            return super.onRequestSendAccessibilityEvent(host, child, event);
1256        }
1257
1258        private void updateFeedbackEnabled() {
1259            mFeedbackEnabled = computeFeedbackEnabled();
1260        }
1261
1262        private boolean computeFeedbackEnabled() {
1263            // are there any enabled non-generic a11y services?
1264            final List<AccessibilityServiceInfo> services =
1265                    mAccessibilityMgr.getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK);
1266            for (AccessibilityServiceInfo asi : services) {
1267                if (asi.feedbackType != 0 && asi.feedbackType != FEEDBACK_GENERIC) {
1268                    return true;
1269                }
1270            }
1271            return false;
1272        }
1273    }
1274
1275    private static class VolumeRow {
1276        private View view;
1277        private TextView header;
1278        private ImageButton icon;
1279        private SeekBar slider;
1280        private int stream;
1281        private StreamState ss;
1282        private long userAttempt;  // last user-driven slider change
1283        private boolean tracking;  // tracking slider touch
1284        private int requestedLevel = -1;  // pending user-requested level via progress changed
1285        private int iconRes;
1286        private int iconMuteRes;
1287        private boolean important;
1288        private int cachedIconRes;
1289        private ColorStateList cachedSliderTint;
1290        private int iconState;  // from Events
1291        private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
1292        private int animTargetProgress;
1293        private int lastAudibleLevel = 1;
1294    }
1295}
1296