OSDN Git Service

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