• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.ActivityManager.LOCK_TASK_MODE_NONE;
20 import static android.media.AudioManager.RINGER_MODE_NORMAL;
21 import static android.media.AudioManager.RINGER_MODE_SILENT;
22 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
23 import static android.media.AudioManager.STREAM_ACCESSIBILITY;
24 import static android.media.AudioManager.STREAM_ALARM;
25 import static android.media.AudioManager.STREAM_MUSIC;
26 import static android.media.AudioManager.STREAM_RING;
27 import static android.media.AudioManager.STREAM_VOICE_CALL;
28 import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE;
29 import static android.view.View.GONE;
30 import static android.view.View.INVISIBLE;
31 import static android.view.View.LAYOUT_DIRECTION_RTL;
32 import static android.view.View.VISIBLE;
33 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
34 
35 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
36 
37 import android.animation.Animator;
38 import android.animation.AnimatorListenerAdapter;
39 import android.animation.ArgbEvaluator;
40 import android.animation.ObjectAnimator;
41 import android.animation.ValueAnimator;
42 import android.annotation.SuppressLint;
43 import android.app.ActivityManager;
44 import android.app.Dialog;
45 import android.app.KeyguardManager;
46 import android.content.ContentResolver;
47 import android.content.Context;
48 import android.content.DialogInterface;
49 import android.content.Intent;
50 import android.content.pm.PackageManager;
51 import android.content.res.ColorStateList;
52 import android.content.res.Configuration;
53 import android.content.res.Resources;
54 import android.content.res.TypedArray;
55 import android.graphics.Color;
56 import android.graphics.Outline;
57 import android.graphics.PixelFormat;
58 import android.graphics.Rect;
59 import android.graphics.Region;
60 import android.graphics.drawable.ColorDrawable;
61 import android.graphics.drawable.Drawable;
62 import android.graphics.drawable.LayerDrawable;
63 import android.graphics.drawable.RotateDrawable;
64 import android.media.AudioManager;
65 import android.media.AudioSystem;
66 import android.os.Debug;
67 import android.os.Handler;
68 import android.os.Looper;
69 import android.os.Message;
70 import android.os.SystemClock;
71 import android.os.VibrationEffect;
72 import android.provider.Settings;
73 import android.provider.Settings.Global;
74 import android.text.InputFilter;
75 import android.util.Log;
76 import android.util.Slog;
77 import android.util.SparseBooleanArray;
78 import android.view.ContextThemeWrapper;
79 import android.view.Gravity;
80 import android.view.MotionEvent;
81 import android.view.View;
82 import android.view.View.AccessibilityDelegate;
83 import android.view.View.OnAttachStateChangeListener;
84 import android.view.ViewGroup;
85 import android.view.ViewOutlineProvider;
86 import android.view.ViewPropertyAnimator;
87 import android.view.ViewStub;
88 import android.view.ViewTreeObserver;
89 import android.view.Window;
90 import android.view.WindowManager;
91 import android.view.accessibility.AccessibilityEvent;
92 import android.view.accessibility.AccessibilityManager;
93 import android.view.accessibility.AccessibilityNodeInfo;
94 import android.view.animation.DecelerateInterpolator;
95 import android.widget.FrameLayout;
96 import android.widget.ImageButton;
97 import android.widget.ImageView;
98 import android.widget.LinearLayout;
99 import android.widget.SeekBar;
100 import android.widget.SeekBar.OnSeekBarChangeListener;
101 import android.widget.TextView;
102 import android.widget.Toast;
103 
104 import androidx.annotation.Nullable;
105 
106 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
107 import com.android.settingslib.Utils;
108 import com.android.systemui.Dependency;
109 import com.android.systemui.Prefs;
110 import com.android.systemui.R;
111 import com.android.systemui.animation.Interpolators;
112 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
113 import com.android.systemui.plugins.ActivityStarter;
114 import com.android.systemui.plugins.VolumeDialog;
115 import com.android.systemui.plugins.VolumeDialogController;
116 import com.android.systemui.plugins.VolumeDialogController.State;
117 import com.android.systemui.plugins.VolumeDialogController.StreamState;
118 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
119 import com.android.systemui.statusbar.policy.ConfigurationController;
120 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
121 import com.android.systemui.util.AlphaTintDrawableWrapper;
122 import com.android.systemui.util.RoundedCornerProgressDrawable;
123 
124 import java.io.PrintWriter;
125 import java.util.ArrayList;
126 import java.util.List;
127 import java.util.function.Consumer;
128 
129 /**
130  * Visual presentation of the volume dialog.
131  *
132  * A client of VolumeDialogControllerImpl and its state model.
133  *
134  * Methods ending in "H" must be called on the (ui) handler.
135  */
136 public class VolumeDialogImpl implements VolumeDialog,
137         ConfigurationController.ConfigurationListener,
138         ViewTreeObserver.OnComputeInternalInsetsListener {
139     private static final String TAG = Util.logTag(VolumeDialogImpl.class);
140 
141     private static final long USER_ATTEMPT_GRACE_PERIOD = 1000;
142     private static final int UPDATE_ANIMATION_DURATION = 80;
143 
144     static final int DIALOG_TIMEOUT_MILLIS = 3000;
145     static final int DIALOG_SAFETYWARNING_TIMEOUT_MILLIS = 5000;
146     static final int DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS = 5000;
147     static final int DIALOG_HOVERING_TIMEOUT_MILLIS = 16000;
148 
149     private static final int DRAWER_ANIMATION_DURATION_SHORT = 175;
150     private static final int DRAWER_ANIMATION_DURATION = 250;
151 
152     private final int mDialogShowAnimationDurationMs;
153     private final int mDialogHideAnimationDurationMs;
154     private int mDialogWidth;
155     private int mDialogCornerRadius;
156     private int mRingerDrawerItemSize;
157     private int mRingerRowsPadding;
158     private boolean mShowVibrate;
159     private int mRingerCount;
160     private final boolean mShowLowMediaVolumeIcon;
161     private final boolean mChangeVolumeRowTintWhenInactive;
162 
163     private final Context mContext;
164     private final H mHandler = new H();
165     private final VolumeDialogController mController;
166     private final DeviceProvisionedController mDeviceProvisionedController;
167     private final Region mTouchableRegion = new Region();
168 
169     private Window mWindow;
170     private CustomDialog mDialog;
171     private ViewGroup mDialogView;
172     private ViewGroup mDialogRowsViewContainer;
173     private ViewGroup mDialogRowsView;
174     private ViewGroup mRinger;
175 
176     /**
177      * Container for the top part of the dialog, which contains the ringer, the ringer drawer, the
178      * volume rows, and the ellipsis button. This does not include the live caption button.
179      */
180     @Nullable private View mTopContainer;
181 
182     /** Container for the ringer icon, and for the (initially hidden) ringer drawer view. */
183     @Nullable private View mRingerAndDrawerContainer;
184 
185     /**
186      * Background drawable for the ringer and drawer container. The background's top bound is
187      * initially inset by the height of the (hidden) ringer drawer. When the drawer is animated in,
188      * this top bound is animated to accommodate it.
189      */
190     @Nullable private Drawable mRingerAndDrawerContainerBackground;
191 
192     private ViewGroup mSelectedRingerContainer;
193     private ImageView mSelectedRingerIcon;
194 
195     private ViewGroup mRingerDrawerContainer;
196     private ViewGroup mRingerDrawerMute;
197     private ViewGroup mRingerDrawerVibrate;
198     private ViewGroup mRingerDrawerNormal;
199     private ImageView mRingerDrawerMuteIcon;
200     private ImageView mRingerDrawerVibrateIcon;
201     private ImageView mRingerDrawerNormalIcon;
202 
203     /**
204      * View that draws the 'selected' background behind one of the three ringer choices in the
205      * drawer.
206      */
207     private ViewGroup mRingerDrawerNewSelectionBg;
208 
209     private final ValueAnimator mRingerDrawerIconColorAnimator = ValueAnimator.ofFloat(0f, 1f);
210     private ImageView mRingerDrawerIconAnimatingSelected;
211     private ImageView mRingerDrawerIconAnimatingDeselected;
212 
213     /**
214      * Animates the volume dialog's background drawable bounds upwards, to match the height of the
215      * expanded ringer drawer.
216      */
217     private final ValueAnimator mAnimateUpBackgroundToMatchDrawer = ValueAnimator.ofFloat(1f, 0f);
218 
219     private boolean mIsRingerDrawerOpen = false;
220     private float mRingerDrawerClosedAmount = 1f;
221 
222     private ImageButton mRingerIcon;
223     private ViewGroup mODICaptionsView;
224     private CaptionsToggleImageButton mODICaptionsIcon;
225     private View mSettingsView;
226     private ImageButton mSettingsIcon;
227     private FrameLayout mZenIcon;
228     private final List<VolumeRow> mRows = new ArrayList<>();
229     private ConfigurableTexts mConfigurableTexts;
230     private final SparseBooleanArray mDynamic = new SparseBooleanArray();
231     private final KeyguardManager mKeyguard;
232     private final ActivityManager mActivityManager;
233     private final AccessibilityManagerWrapper mAccessibilityMgr;
234     private final Object mSafetyWarningLock = new Object();
235     private final Accessibility mAccessibility = new Accessibility();
236 
237     private boolean mShowing;
238     private boolean mShowA11yStream;
239 
240     private int mActiveStream;
241     private int mPrevActiveStream;
242     private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE;
243     private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
244     private State mState;
245     private SafetyWarningDialog mSafetyWarning;
246     private boolean mHovering = false;
247     private boolean mShowActiveStreamOnly;
248     private boolean mConfigChanged = false;
249     private boolean mIsAnimatingDismiss = false;
250     private boolean mHasSeenODICaptionsTooltip;
251     private ViewStub mODICaptionsTooltipViewStub;
252     private View mODICaptionsTooltipView = null;
253 
254     private final boolean mUseBackgroundBlur;
255     private Consumer<Boolean> mCrossWindowBlurEnabledListener;
256     private BackgroundBlurDrawable mDialogRowsViewBackground;
257 
VolumeDialogImpl(Context context)258     public VolumeDialogImpl(Context context) {
259         mContext =
260                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
261         mController = Dependency.get(VolumeDialogController.class);
262         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
263         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
264         mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
265         mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
266         mShowActiveStreamOnly = showActiveStreamOnly();
267         mHasSeenODICaptionsTooltip =
268                 Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
269         mShowLowMediaVolumeIcon =
270             mContext.getResources().getBoolean(R.bool.config_showLowMediaVolumeIcon);
271         mChangeVolumeRowTintWhenInactive =
272             mContext.getResources().getBoolean(R.bool.config_changeVolumeRowTintWhenInactive);
273         mDialogShowAnimationDurationMs =
274             mContext.getResources().getInteger(R.integer.config_dialogShowAnimationDurationMs);
275         mDialogHideAnimationDurationMs =
276             mContext.getResources().getInteger(R.integer.config_dialogHideAnimationDurationMs);
277         mUseBackgroundBlur =
278             mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur);
279 
280         if (mUseBackgroundBlur) {
281             final int dialogRowsViewColorAboveBlur = mContext.getColor(
282                     R.color.volume_dialog_background_color_above_blur);
283             final int dialogRowsViewColorNoBlur = mContext.getColor(
284                     R.color.volume_dialog_background_color);
285             mCrossWindowBlurEnabledListener = (enabled) -> {
286                 mDialogRowsViewBackground.setColor(
287                         enabled ? dialogRowsViewColorAboveBlur : dialogRowsViewColorNoBlur);
288                 mDialogRowsView.invalidate();
289             };
290         }
291 
292         initDimens();
293     }
294 
295     @Override
onUiModeChanged()296     public void onUiModeChanged() {
297         mContext.getTheme().applyStyle(mContext.getThemeResId(), true);
298     }
299 
init(int windowType, Callback callback)300     public void init(int windowType, Callback callback) {
301         initDialog();
302 
303         mAccessibility.init();
304 
305         mController.addCallback(mControllerCallbackH, mHandler);
306         mController.getState();
307 
308         Dependency.get(ConfigurationController.class).addCallback(this);
309     }
310 
311     @Override
destroy()312     public void destroy() {
313         mController.removeCallback(mControllerCallbackH);
314         mHandler.removeCallbacksAndMessages(null);
315         Dependency.get(ConfigurationController.class).removeCallback(this);
316     }
317 
318     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo internalInsetsInfo)319     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo internalInsetsInfo) {
320         // Set touchable region insets on the root dialog view. This tells WindowManager that
321         // touches outside of this region should not be delivered to the volume window, and instead
322         // go to the window below. This is the only way to do this - returning false in
323         // onDispatchTouchEvent results in the event being ignored entirely, rather than passed to
324         // the next window.
325         internalInsetsInfo.setTouchableInsets(
326                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
327 
328         mTouchableRegion.setEmpty();
329 
330         // Set the touchable region to the union of all child view bounds and the live caption
331         // tooltip. We don't use touches on the volume dialog container itself, so this is fine.
332         for (int i = 0; i < mDialogView.getChildCount(); i++) {
333             unionViewBoundstoTouchableRegion(mDialogView.getChildAt(i));
334         }
335 
336         if (mODICaptionsTooltipView != null && mODICaptionsTooltipView.getVisibility() == VISIBLE) {
337             unionViewBoundstoTouchableRegion(mODICaptionsTooltipView);
338         }
339 
340         internalInsetsInfo.touchableRegion.set(mTouchableRegion);
341     }
342 
unionViewBoundstoTouchableRegion(final View view)343     private void unionViewBoundstoTouchableRegion(final View view) {
344         final int[] locInWindow = new int[2];
345         view.getLocationInWindow(locInWindow);
346 
347         float x = locInWindow[0];
348         float y = locInWindow[1];
349 
350         // The ringer and rows container has extra height at the top to fit the expanded ringer
351         // drawer. This area should not be touchable unless the ringer drawer is open.
352         if (view == mTopContainer && !mIsRingerDrawerOpen) {
353             if (!isLandscape()) {
354                 y += getRingerDrawerOpenExtraSize();
355             } else {
356                 x += getRingerDrawerOpenExtraSize();
357             }
358         }
359 
360         mTouchableRegion.op(
361                 (int) x,
362                 (int) y,
363                 locInWindow[0] + view.getWidth(),
364                 locInWindow[1] + view.getHeight(),
365                 Region.Op.UNION);
366     }
367 
initDialog()368     private void initDialog() {
369         mDialog = new CustomDialog(mContext);
370 
371         initDimens();
372 
373         mConfigurableTexts = new ConfigurableTexts(mContext);
374         mHovering = false;
375         mShowing = false;
376         mWindow = mDialog.getWindow();
377         mWindow.requestFeature(Window.FEATURE_NO_TITLE);
378         mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
379         mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
380                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
381         mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
382                 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
383                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
384                 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
385                 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
386         mWindow.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY);
387         mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
388         mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
389         WindowManager.LayoutParams lp = mWindow.getAttributes();
390         lp.format = PixelFormat.TRANSLUCENT;
391         lp.setTitle(VolumeDialogImpl.class.getSimpleName());
392         lp.windowAnimations = -1;
393         lp.gravity = mContext.getResources().getInteger(R.integer.volume_dialog_gravity);
394         mWindow.setAttributes(lp);
395         mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT);
396 
397         mDialog.setContentView(R.layout.volume_dialog);
398         mDialogView = mDialog.findViewById(R.id.volume_dialog);
399         mDialogView.setAlpha(0);
400         mDialog.setCanceledOnTouchOutside(true);
401         mDialog.setOnShowListener(dialog -> {
402             mDialogView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
403             if (!isLandscape()) mDialogView.setTranslationX(mDialogView.getWidth() / 2.0f);
404             mDialogView.setAlpha(0);
405             mDialogView.animate()
406                     .alpha(1)
407                     .translationX(0)
408                     .setDuration(mDialogShowAnimationDurationMs)
409                     .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
410                     .withEndAction(() -> {
411                         if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
412                             if (mRingerIcon != null) {
413                                 mRingerIcon.postOnAnimationDelayed(
414                                         getSinglePressFor(mRingerIcon), 1500);
415                             }
416                         }
417                     })
418                     .start();
419         });
420 
421         mDialog.setOnDismissListener(dialogInterface ->
422                 mDialogView
423                         .getViewTreeObserver()
424                         .removeOnComputeInternalInsetsListener(VolumeDialogImpl.this));
425 
426         mDialogView.setOnHoverListener((v, event) -> {
427             int action = event.getActionMasked();
428             mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
429                     || (action == MotionEvent.ACTION_HOVER_MOVE);
430             rescheduleTimeoutH();
431             return true;
432         });
433 
434         mDialogRowsView = mDialog.findViewById(R.id.volume_dialog_rows);
435         if (mUseBackgroundBlur) {
436             mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
437                 @Override
438                 public void onViewAttachedToWindow(View v) {
439                     mWindow.getWindowManager().addCrossWindowBlurEnabledListener(
440                             mCrossWindowBlurEnabledListener);
441 
442                     mDialogRowsViewBackground = v.getViewRootImpl().createBackgroundBlurDrawable();
443 
444                     final Resources resources = mContext.getResources();
445                     mDialogRowsViewBackground.setCornerRadius(
446                             mContext.getResources().getDimensionPixelSize(Utils.getThemeAttr(
447                                     mContext, android.R.attr.dialogCornerRadius)));
448                     mDialogRowsViewBackground.setBlurRadius(resources.getDimensionPixelSize(
449                             R.dimen.volume_dialog_background_blur_radius));
450                     mDialogRowsView.setBackground(mDialogRowsViewBackground);
451                 }
452 
453                 @Override
454                 public void onViewDetachedFromWindow(View v) {
455                     mWindow.getWindowManager().removeCrossWindowBlurEnabledListener(
456                             mCrossWindowBlurEnabledListener);
457                 }
458             });
459         }
460 
461         mDialogRowsViewContainer = mDialogView.findViewById(R.id.volume_dialog_rows_container);
462         mTopContainer = mDialogView.findViewById(R.id.volume_dialog_top_container);
463         mRingerAndDrawerContainer = mDialogView.findViewById(
464                 R.id.volume_ringer_and_drawer_container);
465 
466         if (mRingerAndDrawerContainer != null) {
467             if (isLandscape()) {
468                 // In landscape, we need to add padding to the bottom of the ringer drawer so that
469                 // when it expands to the left, it doesn't overlap any additional volume rows.
470                 mRingerAndDrawerContainer.setPadding(
471                         mRingerAndDrawerContainer.getPaddingLeft(),
472                         mRingerAndDrawerContainer.getPaddingTop(),
473                         mRingerAndDrawerContainer.getPaddingRight(),
474                         mRingerRowsPadding);
475 
476                 // Since the ringer drawer is expanding to the left, outside of the background of
477                 // the dialog, it needs its own rounded background drawable. We also need that
478                 // background to be rounded on all sides. We'll use a background rounded on all four
479                 // corners, and then extend the container's background later to fill in the bottom
480                 // corners when the drawer is closed.
481                 mRingerAndDrawerContainer.setBackgroundDrawable(
482                         mContext.getDrawable(R.drawable.volume_background_top_rounded));
483             }
484 
485             // Post to wait for layout so that the background bounds are set.
486             mRingerAndDrawerContainer.post(() -> {
487                 final LayerDrawable ringerAndDrawerBg =
488                         (LayerDrawable) mRingerAndDrawerContainer.getBackground();
489 
490                 // Retrieve the ShapeDrawable from within the background - this is what we will
491                 // animate up and down when the drawer is opened/closed.
492                 if (ringerAndDrawerBg != null && ringerAndDrawerBg.getNumberOfLayers() > 0) {
493                     mRingerAndDrawerContainerBackground = ringerAndDrawerBg.getDrawable(0);
494 
495                     updateBackgroundForDrawerClosedAmount();
496                     setTopContainerBackgroundDrawable();
497                 }
498             });
499         }
500 
501         mRinger = mDialog.findViewById(R.id.ringer);
502         if (mRinger != null) {
503             mRingerIcon = mRinger.findViewById(R.id.ringer_icon);
504             mZenIcon = mRinger.findViewById(R.id.dnd_icon);
505         }
506 
507         mSelectedRingerIcon = mDialog.findViewById(R.id.volume_new_ringer_active_icon);
508         mSelectedRingerContainer = mDialog.findViewById(
509                 R.id.volume_new_ringer_active_icon_container);
510 
511         mRingerDrawerMute = mDialog.findViewById(R.id.volume_drawer_mute);
512         mRingerDrawerNormal = mDialog.findViewById(R.id.volume_drawer_normal);
513         mRingerDrawerVibrate = mDialog.findViewById(R.id.volume_drawer_vibrate);
514         mRingerDrawerMuteIcon = mDialog.findViewById(R.id.volume_drawer_mute_icon);
515         mRingerDrawerVibrateIcon = mDialog.findViewById(R.id.volume_drawer_vibrate_icon);
516         mRingerDrawerNormalIcon = mDialog.findViewById(R.id.volume_drawer_normal_icon);
517         mRingerDrawerNewSelectionBg = mDialog.findViewById(R.id.volume_drawer_selection_background);
518 
519         setupRingerDrawer();
520 
521         mODICaptionsView = mDialog.findViewById(R.id.odi_captions);
522         if (mODICaptionsView != null) {
523             mODICaptionsIcon = mODICaptionsView.findViewById(R.id.odi_captions_icon);
524         }
525         mODICaptionsTooltipViewStub = mDialog.findViewById(R.id.odi_captions_tooltip_stub);
526         if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipViewStub != null) {
527             mDialogView.removeView(mODICaptionsTooltipViewStub);
528             mODICaptionsTooltipViewStub = null;
529         }
530 
531         mSettingsView = mDialog.findViewById(R.id.settings_container);
532         mSettingsIcon = mDialog.findViewById(R.id.settings);
533 
534         if (mRows.isEmpty()) {
535             if (!AudioSystem.isSingleVolume(mContext)) {
536                 addRow(STREAM_ACCESSIBILITY, R.drawable.ic_volume_accessibility,
537                         R.drawable.ic_volume_accessibility, true, false);
538             }
539             addRow(AudioManager.STREAM_MUSIC,
540                     R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
541             if (!AudioSystem.isSingleVolume(mContext)) {
542                 addRow(AudioManager.STREAM_RING,
543                         R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true, false);
544                 addRow(STREAM_ALARM,
545                         R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
546                 addRow(AudioManager.STREAM_VOICE_CALL,
547                         com.android.internal.R.drawable.ic_phone,
548                         com.android.internal.R.drawable.ic_phone, false, false);
549                 addRow(AudioManager.STREAM_BLUETOOTH_SCO,
550                         R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false, false);
551                 addRow(AudioManager.STREAM_SYSTEM, R.drawable.ic_volume_system,
552                         R.drawable.ic_volume_system_mute, false, false);
553             }
554         } else {
555             addExistingRows();
556         }
557 
558         updateRowsH(getActiveRow());
559         initRingerH();
560         initSettingsH();
561         initODICaptionsH();
562     }
563 
initDimens()564     private void initDimens() {
565         mDialogWidth = mContext.getResources().getDimensionPixelSize(
566                 R.dimen.volume_dialog_panel_width);
567         mDialogCornerRadius = mContext.getResources().getDimensionPixelSize(
568                 R.dimen.volume_dialog_panel_width_half);
569         mRingerDrawerItemSize = mContext.getResources().getDimensionPixelSize(
570                 R.dimen.volume_ringer_drawer_item_size);
571         mRingerRowsPadding = mContext.getResources().getDimensionPixelSize(
572                 R.dimen.volume_dialog_ringer_rows_padding);
573         mShowVibrate = mController.hasVibrator();
574 
575         // Normal, mute, and possibly vibrate.
576         mRingerCount = mShowVibrate ? 3 : 2;
577     }
578 
getDialogView()579     protected ViewGroup getDialogView() {
580         return mDialogView;
581     }
582 
getAlphaAttr(int attr)583     private int getAlphaAttr(int attr) {
584         TypedArray ta = mContext.obtainStyledAttributes(new int[]{attr});
585         float alpha = ta.getFloat(0, 0);
586         ta.recycle();
587         return (int) (alpha * 255);
588     }
589 
isLandscape()590     private boolean isLandscape() {
591         return mContext.getResources().getConfiguration().orientation ==
592                 Configuration.ORIENTATION_LANDSCAPE;
593     }
594 
isRtl()595     private boolean isRtl() {
596         return mContext.getResources().getConfiguration().getLayoutDirection()
597                 == LAYOUT_DIRECTION_RTL;
598     }
599 
setStreamImportant(int stream, boolean important)600     public void setStreamImportant(int stream, boolean important) {
601         mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget();
602     }
603 
setAutomute(boolean automute)604     public void setAutomute(boolean automute) {
605         if (mAutomute == automute) return;
606         mAutomute = automute;
607         mHandler.sendEmptyMessage(H.RECHECK_ALL);
608     }
609 
setSilentMode(boolean silentMode)610     public void setSilentMode(boolean silentMode) {
611         if (mSilentMode == silentMode) return;
612         mSilentMode = silentMode;
613         mHandler.sendEmptyMessage(H.RECHECK_ALL);
614     }
615 
addRow(int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream)616     private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
617             boolean defaultStream) {
618         addRow(stream, iconRes, iconMuteRes, important, defaultStream, false);
619     }
620 
addRow(int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream, boolean dynamic)621     private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
622             boolean defaultStream, boolean dynamic) {
623         if (D.BUG) Slog.d(TAG, "Adding row for stream " + stream);
624         VolumeRow row = new VolumeRow();
625         initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
626         mDialogRowsView.addView(row.view);
627         mRows.add(row);
628     }
629 
addExistingRows()630     private void addExistingRows() {
631         int N = mRows.size();
632         for (int i = 0; i < N; i++) {
633             final VolumeRow row = mRows.get(i);
634             initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important,
635                     row.defaultStream);
636             mDialogRowsView.addView(row.view);
637             updateVolumeRowH(row);
638         }
639     }
640 
getActiveRow()641     private VolumeRow getActiveRow() {
642         for (VolumeRow row : mRows) {
643             if (row.stream == mActiveStream) {
644                 return row;
645             }
646         }
647         for (VolumeRow row : mRows) {
648             if (row.stream == STREAM_MUSIC) {
649                 return row;
650             }
651         }
652         return mRows.get(0);
653     }
654 
findRow(int stream)655     private VolumeRow findRow(int stream) {
656         for (VolumeRow row : mRows) {
657             if (row.stream == stream) return row;
658         }
659         return null;
660     }
661 
dump(PrintWriter writer)662     public void dump(PrintWriter writer) {
663         writer.println(VolumeDialogImpl.class.getSimpleName() + " state:");
664         writer.print("  mShowing: "); writer.println(mShowing);
665         writer.print("  mActiveStream: "); writer.println(mActiveStream);
666         writer.print("  mDynamic: "); writer.println(mDynamic);
667         writer.print("  mAutomute: "); writer.println(mAutomute);
668         writer.print("  mSilentMode: "); writer.println(mSilentMode);
669     }
670 
getImpliedLevel(SeekBar seekBar, int progress)671     private static int getImpliedLevel(SeekBar seekBar, int progress) {
672         final int m = seekBar.getMax();
673         final int n = m / 100 - 1;
674         final int level = progress == 0 ? 0
675                 : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n));
676         return level;
677     }
678 
679     @SuppressLint("InflateParams")
initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream)680     private void initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes,
681             boolean important, boolean defaultStream) {
682         row.stream = stream;
683         row.iconRes = iconRes;
684         row.iconMuteRes = iconMuteRes;
685         row.important = important;
686         row.defaultStream = defaultStream;
687         row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null);
688         row.view.setId(row.stream);
689         row.view.setTag(row);
690         row.header = row.view.findViewById(R.id.volume_row_header);
691         row.header.setId(20 * row.stream);
692         if (stream == STREAM_ACCESSIBILITY) {
693             row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)});
694         }
695         row.dndIcon = row.view.findViewById(R.id.dnd_icon);
696         row.slider = row.view.findViewById(R.id.volume_row_slider);
697         row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
698         row.number = row.view.findViewById(R.id.volume_number);
699 
700         row.anim = null;
701 
702         final LayerDrawable seekbarDrawable =
703                 (LayerDrawable) mContext.getDrawable(R.drawable.volume_row_seekbar);
704 
705         final LayerDrawable seekbarProgressDrawable = (LayerDrawable)
706                 ((RoundedCornerProgressDrawable) seekbarDrawable.findDrawableByLayerId(
707                         android.R.id.progress)).getDrawable();
708 
709         row.sliderProgressSolid = seekbarProgressDrawable.findDrawableByLayerId(
710                 R.id.volume_seekbar_progress_solid);
711         final Drawable sliderProgressIcon = seekbarProgressDrawable.findDrawableByLayerId(
712                         R.id.volume_seekbar_progress_icon);
713         row.sliderProgressIcon = sliderProgressIcon != null ? (AlphaTintDrawableWrapper)
714                 ((RotateDrawable) sliderProgressIcon).getDrawable() : null;
715 
716         row.slider.setProgressDrawable(seekbarDrawable);
717 
718         row.icon = row.view.findViewById(R.id.volume_row_icon);
719 
720         row.setIcon(iconRes, mContext.getTheme());
721 
722         if (row.icon != null) {
723             if (row.stream != AudioSystem.STREAM_ACCESSIBILITY) {
724                 row.icon.setOnClickListener(v -> {
725                     Events.writeEvent(Events.EVENT_ICON_CLICK, row.stream, row.iconState);
726                     mController.setActiveStream(row.stream);
727                     if (row.stream == AudioManager.STREAM_RING) {
728                         final boolean hasVibrator = mController.hasVibrator();
729                         if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
730                             if (hasVibrator) {
731                                 mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
732                             } else {
733                                 final boolean wasZero = row.ss.level == 0;
734                                 mController.setStreamVolume(stream,
735                                         wasZero ? row.lastAudibleLevel : 0);
736                             }
737                         } else {
738                             mController.setRingerMode(
739                                     AudioManager.RINGER_MODE_NORMAL, false);
740                             if (row.ss.level == 0) {
741                                 mController.setStreamVolume(stream, 1);
742                             }
743                         }
744                     } else {
745                         final boolean vmute = row.ss.level == row.ss.levelMin;
746                         mController.setStreamVolume(stream,
747                                 vmute ? row.lastAudibleLevel : row.ss.levelMin);
748                     }
749                     row.userAttempt = 0;  // reset the grace period, slider updates immediately
750                 });
751             } else {
752                 row.icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
753             }
754         }
755     }
756 
setRingerMode(int newRingerMode)757     private void setRingerMode(int newRingerMode) {
758         Events.writeEvent(Events.EVENT_RINGER_TOGGLE, newRingerMode);
759         incrementManualToggleCount();
760         updateRingerH();
761         provideTouchFeedbackH(newRingerMode);
762         mController.setRingerMode(newRingerMode, false);
763         maybeShowToastH(newRingerMode);
764     }
765 
setupRingerDrawer()766     private void setupRingerDrawer() {
767         mRingerDrawerContainer = mDialog.findViewById(R.id.volume_drawer_container);
768 
769         if (mRingerDrawerContainer == null) {
770             return;
771         }
772 
773         if (!mShowVibrate) {
774             mRingerDrawerVibrate.setVisibility(GONE);
775         }
776 
777         // In portrait, add padding to the bottom to account for the height of the open ringer
778         // drawer.
779         if (!isLandscape()) {
780             mDialogView.setPadding(
781                     mDialogView.getPaddingLeft(),
782                     mDialogView.getPaddingTop(),
783                     mDialogView.getPaddingRight(),
784                     mDialogView.getPaddingBottom() + getRingerDrawerOpenExtraSize());
785         } else {
786             mDialogView.setPadding(
787                     mDialogView.getPaddingLeft() + getRingerDrawerOpenExtraSize(),
788                     mDialogView.getPaddingTop(),
789                     mDialogView.getPaddingRight(),
790                     mDialogView.getPaddingBottom());
791         }
792 
793         ((LinearLayout) mRingerDrawerContainer.findViewById(R.id.volume_drawer_options))
794                 .setOrientation(isLandscape() ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
795 
796         mSelectedRingerContainer.setOnClickListener(view -> {
797             if (mIsRingerDrawerOpen) {
798                 hideRingerDrawer();
799             } else {
800                 showRingerDrawer();
801             }
802         });
803 
804         mRingerDrawerVibrate.setOnClickListener(
805                 new RingerDrawerItemClickListener(RINGER_MODE_VIBRATE));
806         mRingerDrawerMute.setOnClickListener(
807                 new RingerDrawerItemClickListener(RINGER_MODE_SILENT));
808         mRingerDrawerNormal.setOnClickListener(
809                 new RingerDrawerItemClickListener(RINGER_MODE_NORMAL));
810 
811         final int unselectedColor = Utils.getColorAccentDefaultColor(mContext);
812         final int selectedColor = Utils.getColorAttrDefaultColor(
813                 mContext, android.R.attr.colorBackgroundFloating);
814 
815         // Add an update listener that animates the deselected icon to the unselected color, and the
816         // selected icon to the selected color.
817         mRingerDrawerIconColorAnimator.addUpdateListener(
818                 anim -> {
819                     final float currentValue = (float) anim.getAnimatedValue();
820                     final int curUnselectedColor = (int) ArgbEvaluator.getInstance().evaluate(
821                             currentValue, selectedColor, unselectedColor);
822                     final int curSelectedColor = (int) ArgbEvaluator.getInstance().evaluate(
823                             currentValue, unselectedColor, selectedColor);
824 
825                     mRingerDrawerIconAnimatingDeselected.setColorFilter(curUnselectedColor);
826                     mRingerDrawerIconAnimatingSelected.setColorFilter(curSelectedColor);
827                 });
828         mRingerDrawerIconColorAnimator.addListener(new AnimatorListenerAdapter() {
829             @Override
830             public void onAnimationEnd(Animator animation) {
831                 mRingerDrawerIconAnimatingDeselected.clearColorFilter();
832                 mRingerDrawerIconAnimatingSelected.clearColorFilter();
833             }
834         });
835         mRingerDrawerIconColorAnimator.setDuration(DRAWER_ANIMATION_DURATION_SHORT);
836 
837         mAnimateUpBackgroundToMatchDrawer.addUpdateListener(valueAnimator -> {
838             mRingerDrawerClosedAmount = (float) valueAnimator.getAnimatedValue();
839             updateBackgroundForDrawerClosedAmount();
840         });
841     }
842 
getDrawerIconViewForMode(int mode)843     private ImageView getDrawerIconViewForMode(int mode) {
844         if (mode == RINGER_MODE_VIBRATE) {
845             return mRingerDrawerVibrateIcon;
846         } else if (mode == RINGER_MODE_SILENT) {
847             return mRingerDrawerMuteIcon;
848         } else {
849             return mRingerDrawerNormalIcon;
850         }
851     }
852 
853     /**
854      * Translation to apply form the origin (either top or left) to overlap the selection background
855      * with the given mode in the drawer.
856      */
getTranslationInDrawerForRingerMode(int mode)857     private float getTranslationInDrawerForRingerMode(int mode) {
858         return mode == RINGER_MODE_VIBRATE
859                 ? -mRingerDrawerItemSize * 2
860                 : mode == RINGER_MODE_SILENT
861                         ? -mRingerDrawerItemSize
862                         : 0;
863     }
864 
865     /** Animates in the ringer drawer. */
showRingerDrawer()866     private void showRingerDrawer() {
867         if (mIsRingerDrawerOpen) {
868             return;
869         }
870 
871         // Show all ringer icons except the currently selected one, since we're going to animate the
872         // ringer button to that position.
873         mRingerDrawerVibrateIcon.setVisibility(
874                 mState.ringerModeInternal == RINGER_MODE_VIBRATE ? INVISIBLE : VISIBLE);
875         mRingerDrawerMuteIcon.setVisibility(
876                 mState.ringerModeInternal == RINGER_MODE_SILENT ? INVISIBLE : VISIBLE);
877         mRingerDrawerNormalIcon.setVisibility(
878                 mState.ringerModeInternal == RINGER_MODE_NORMAL ? INVISIBLE : VISIBLE);
879 
880         // Hide the selection background - we use this to show a selection when one is
881         // tapped, so it should be invisible until that happens. However, position it below
882         // the currently selected ringer so that it's ready to animate.
883         mRingerDrawerNewSelectionBg.setAlpha(0f);
884 
885         if (!isLandscape()) {
886             mRingerDrawerNewSelectionBg.setTranslationY(
887                     getTranslationInDrawerForRingerMode(mState.ringerModeInternal));
888         } else {
889             mRingerDrawerNewSelectionBg.setTranslationX(
890                     getTranslationInDrawerForRingerMode(mState.ringerModeInternal));
891         }
892 
893         // Move the drawer so that the top/rightmost ringer choice overlaps with the selected ringer
894         // icon.
895         if (!isLandscape()) {
896             mRingerDrawerContainer.setTranslationY(mRingerDrawerItemSize * (mRingerCount - 1));
897         } else {
898             mRingerDrawerContainer.setTranslationX(mRingerDrawerItemSize * (mRingerCount - 1));
899         }
900         mRingerDrawerContainer.setAlpha(0f);
901         mRingerDrawerContainer.setVisibility(VISIBLE);
902 
903         final int ringerDrawerAnimationDuration = mState.ringerModeInternal == RINGER_MODE_VIBRATE
904                 ? DRAWER_ANIMATION_DURATION_SHORT
905                 : DRAWER_ANIMATION_DURATION;
906 
907         // Animate the drawer up and visible.
908         mRingerDrawerContainer.animate()
909                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
910                 // Vibrate is way farther up, so give the selected ringer icon a head start if
911                 // vibrate is selected.
912                 .setDuration(ringerDrawerAnimationDuration)
913                 .setStartDelay(mState.ringerModeInternal == RINGER_MODE_VIBRATE
914                         ? DRAWER_ANIMATION_DURATION - DRAWER_ANIMATION_DURATION_SHORT
915                         : 0)
916                 .alpha(1f)
917                 .translationX(0f)
918                 .translationY(0f)
919                 .start();
920 
921         // Animate the selected ringer view up to that ringer's position in the drawer.
922         mSelectedRingerContainer.animate()
923                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
924                 .setDuration(DRAWER_ANIMATION_DURATION)
925                 .withEndAction(() ->
926                         getDrawerIconViewForMode(mState.ringerModeInternal).setVisibility(VISIBLE));
927 
928         mAnimateUpBackgroundToMatchDrawer.setDuration(ringerDrawerAnimationDuration);
929         mAnimateUpBackgroundToMatchDrawer.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
930         mAnimateUpBackgroundToMatchDrawer.start();
931 
932         if (!isLandscape()) {
933             mSelectedRingerContainer.animate()
934                     .translationY(getTranslationInDrawerForRingerMode(mState.ringerModeInternal))
935                     .start();
936         } else {
937             mSelectedRingerContainer.animate()
938                     .translationX(getTranslationInDrawerForRingerMode(mState.ringerModeInternal))
939                     .start();
940         }
941 
942         // When the ringer drawer is open, tapping the currently selected ringer will set the ringer
943         // to the current ringer mode. Change the content description to that, instead of the 'tap
944         // to change ringer mode' default.
945         mSelectedRingerContainer.setContentDescription(
946                 mContext.getString(getStringDescriptionResourceForRingerMode(
947                         mState.ringerModeInternal)));
948 
949         mIsRingerDrawerOpen = true;
950     }
951 
952     /** Animates away the ringer drawer. */
hideRingerDrawer()953     private void hideRingerDrawer() {
954 
955         // If the ringer drawer isn't present, don't try to hide it.
956         if (mRingerDrawerContainer == null) {
957             return;
958         }
959 
960         if (!mIsRingerDrawerOpen) {
961             return;
962         }
963 
964         // Hide the drawer icon for the selected ringer - it's visible in the ringer button and we
965         // don't want to be able to see it while it animates away.
966         getDrawerIconViewForMode(mState.ringerModeInternal).setVisibility(INVISIBLE);
967 
968         mRingerDrawerContainer.animate()
969                 .alpha(0f)
970                 .setDuration(DRAWER_ANIMATION_DURATION)
971                 .setStartDelay(0)
972                 .withEndAction(() -> mRingerDrawerContainer.setVisibility(INVISIBLE));
973 
974         if (!isLandscape()) {
975             mRingerDrawerContainer.animate()
976                     .translationY(mRingerDrawerItemSize * 2)
977                     .start();
978         } else {
979             mRingerDrawerContainer.animate()
980                     .translationX(mRingerDrawerItemSize * 2)
981                     .start();
982         }
983 
984         mAnimateUpBackgroundToMatchDrawer.setDuration(DRAWER_ANIMATION_DURATION);
985         mAnimateUpBackgroundToMatchDrawer.setInterpolator(Interpolators.FAST_OUT_SLOW_IN_REVERSE);
986         mAnimateUpBackgroundToMatchDrawer.reverse();
987 
988         mSelectedRingerContainer.animate()
989                 .translationX(0f)
990                 .translationY(0f)
991                 .start();
992 
993         // When the drawer is closed, tapping the selected ringer drawer will open it, allowing the
994         // user to change the ringer.
995         mSelectedRingerContainer.setContentDescription(
996                 mContext.getString(R.string.volume_ringer_change));
997 
998         mIsRingerDrawerOpen = false;
999     }
1000 
initSettingsH()1001     public void initSettingsH() {
1002         if (mSettingsView != null) {
1003             mSettingsView.setVisibility(
1004                     mDeviceProvisionedController.isCurrentUserSetup() &&
1005                             mActivityManager.getLockTaskModeState() == LOCK_TASK_MODE_NONE ?
1006                             VISIBLE : GONE);
1007         }
1008         if (mSettingsIcon != null) {
1009             mSettingsIcon.setOnClickListener(v -> {
1010                 Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
1011                 Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
1012                 dismissH(DISMISS_REASON_SETTINGS_CLICKED);
1013                 Dependency.get(MediaOutputDialogFactory.class).dismiss();
1014                 Dependency.get(ActivityStarter.class).startActivity(intent,
1015                         true /* dismissShade */);
1016             });
1017         }
1018     }
1019 
initRingerH()1020     public void initRingerH() {
1021         if (mRingerIcon != null) {
1022             mRingerIcon.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
1023             mRingerIcon.setOnClickListener(v -> {
1024                 Prefs.putBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true);
1025                 final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
1026                 if (ss == null) {
1027                     return;
1028                 }
1029                 // normal -> vibrate -> silent -> normal (skip vibrate if device doesn't have
1030                 // a vibrator.
1031                 int newRingerMode;
1032                 final boolean hasVibrator = mController.hasVibrator();
1033                 if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
1034                     if (hasVibrator) {
1035                         newRingerMode = AudioManager.RINGER_MODE_VIBRATE;
1036                     } else {
1037                         newRingerMode = AudioManager.RINGER_MODE_SILENT;
1038                     }
1039                 } else if (mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
1040                     newRingerMode = AudioManager.RINGER_MODE_SILENT;
1041                 } else {
1042                     newRingerMode = AudioManager.RINGER_MODE_NORMAL;
1043                     if (ss.level == 0) {
1044                         mController.setStreamVolume(AudioManager.STREAM_RING, 1);
1045                     }
1046                 }
1047 
1048                 setRingerMode(newRingerMode);
1049             });
1050         }
1051         updateRingerH();
1052     }
1053 
initODICaptionsH()1054     private void initODICaptionsH() {
1055         if (mODICaptionsIcon != null) {
1056             mODICaptionsIcon.setOnConfirmedTapListener(() -> {
1057                 onCaptionIconClicked();
1058                 Events.writeEvent(Events.EVENT_ODI_CAPTIONS_CLICK);
1059             }, mHandler);
1060         }
1061 
1062         mController.getCaptionsComponentState(false);
1063     }
1064 
checkODICaptionsTooltip(boolean fromDismiss)1065     private void checkODICaptionsTooltip(boolean fromDismiss) {
1066         if (!mHasSeenODICaptionsTooltip && !fromDismiss && mODICaptionsTooltipViewStub != null) {
1067             mController.getCaptionsComponentState(true);
1068         } else {
1069             if (mHasSeenODICaptionsTooltip && fromDismiss && mODICaptionsTooltipView != null) {
1070                 hideCaptionsTooltip();
1071             }
1072         }
1073     }
1074 
showCaptionsTooltip()1075     protected void showCaptionsTooltip() {
1076         if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipViewStub != null) {
1077             mODICaptionsTooltipView = mODICaptionsTooltipViewStub.inflate();
1078             mODICaptionsTooltipView.findViewById(R.id.dismiss).setOnClickListener(v -> {
1079                 hideCaptionsTooltip();
1080                 Events.writeEvent(Events.EVENT_ODI_CAPTIONS_TOOLTIP_CLICK);
1081             });
1082             mODICaptionsTooltipViewStub = null;
1083             rescheduleTimeoutH();
1084         }
1085 
1086         if (mODICaptionsTooltipView != null) {
1087             mODICaptionsTooltipView.setAlpha(0.0f);
1088 
1089             // We need to wait for layout and then center the caption view. Since the height of the
1090             // dialog is now dynamic (with the variable ringer drawer height changing the height of
1091             // the dialog), we need to do this here in code vs. in XML.
1092             mHandler.post(() -> {
1093                 final int[] odiTooltipLocation = mODICaptionsTooltipView.getLocationOnScreen();
1094                 final int[] odiButtonLocation = mODICaptionsIcon.getLocationOnScreen();
1095 
1096                 final float heightDiffForCentering =
1097                         (mODICaptionsTooltipView.getHeight() - mODICaptionsIcon.getHeight()) / 2f;
1098 
1099                 mODICaptionsTooltipView.setTranslationY(
1100                         odiButtonLocation[1] - odiTooltipLocation[1] - heightDiffForCentering);
1101 
1102                 mODICaptionsTooltipView.animate()
1103                         .alpha(1.0f)
1104                         .setStartDelay(mDialogShowAnimationDurationMs)
1105                         .withEndAction(() -> {
1106                             if (D.BUG) {
1107                                 Log.d(TAG, "tool:checkODICaptionsTooltip() putBoolean true");
1108                             }
1109                             Prefs.putBoolean(mContext,
1110                                     Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, true);
1111                             mHasSeenODICaptionsTooltip = true;
1112                             if (mODICaptionsIcon != null) {
1113                                 mODICaptionsIcon
1114                                         .postOnAnimation(getSinglePressFor(mODICaptionsIcon));
1115                             }
1116                         })
1117                         .start();
1118             });
1119         }
1120     }
1121 
hideCaptionsTooltip()1122     private void hideCaptionsTooltip() {
1123         if (mODICaptionsTooltipView != null && mODICaptionsTooltipView.getVisibility() == VISIBLE) {
1124             mODICaptionsTooltipView.animate().cancel();
1125             mODICaptionsTooltipView.setAlpha(1.f);
1126             mODICaptionsTooltipView.animate()
1127                     .alpha(0.f)
1128                     .setStartDelay(0)
1129                     .setDuration(mDialogHideAnimationDurationMs)
1130                     .withEndAction(() -> {
1131                         // It might have been nulled out by tryToRemoveCaptionsTooltip.
1132                         if (mODICaptionsTooltipView != null) {
1133                             mODICaptionsTooltipView.setVisibility(INVISIBLE);
1134                         }
1135                     })
1136                     .start();
1137         }
1138     }
1139 
tryToRemoveCaptionsTooltip()1140     protected void tryToRemoveCaptionsTooltip() {
1141         if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) {
1142             ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container);
1143             container.removeView(mODICaptionsTooltipView);
1144             mODICaptionsTooltipView = null;
1145         }
1146     }
1147 
updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip)1148     private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) {
1149         if (mODICaptionsView != null) {
1150             mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE);
1151         }
1152 
1153         if (!isServiceComponentEnabled) return;
1154 
1155         updateCaptionsIcon();
1156         if (fromTooltip) showCaptionsTooltip();
1157     }
1158 
updateCaptionsIcon()1159     private void updateCaptionsIcon() {
1160         boolean captionsEnabled = mController.areCaptionsEnabled();
1161         if (mODICaptionsIcon.getCaptionsEnabled() != captionsEnabled) {
1162             mHandler.post(mODICaptionsIcon.setCaptionsEnabled(captionsEnabled));
1163         }
1164 
1165         boolean isOptedOut = mController.isCaptionStreamOptedOut();
1166         if (mODICaptionsIcon.getOptedOut() != isOptedOut) {
1167             mHandler.post(() -> mODICaptionsIcon.setOptedOut(isOptedOut));
1168         }
1169     }
1170 
onCaptionIconClicked()1171     private void onCaptionIconClicked() {
1172         boolean isEnabled = mController.areCaptionsEnabled();
1173         mController.setCaptionsEnabled(!isEnabled);
1174         updateCaptionsIcon();
1175     }
1176 
incrementManualToggleCount()1177     private void incrementManualToggleCount() {
1178         ContentResolver cr = mContext.getContentResolver();
1179         int ringerCount = Settings.Secure.getInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, 0);
1180         Settings.Secure.putInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, ringerCount + 1);
1181     }
1182 
provideTouchFeedbackH(int newRingerMode)1183     private void provideTouchFeedbackH(int newRingerMode) {
1184         VibrationEffect effect = null;
1185         switch (newRingerMode) {
1186             case RINGER_MODE_NORMAL:
1187                 mController.scheduleTouchFeedback();
1188                 break;
1189             case RINGER_MODE_SILENT:
1190                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
1191                 break;
1192             case RINGER_MODE_VIBRATE:
1193             default:
1194                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
1195         }
1196         if (effect != null) {
1197             mController.vibrate(effect);
1198         }
1199     }
1200 
maybeShowToastH(int newRingerMode)1201     private void maybeShowToastH(int newRingerMode) {
1202         int seenToastCount = Prefs.getInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, 0);
1203 
1204         if (seenToastCount > VolumePrefs.SHOW_RINGER_TOAST_COUNT) {
1205             return;
1206         }
1207         CharSequence toastText = null;
1208         switch (newRingerMode) {
1209             case RINGER_MODE_NORMAL:
1210                 final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
1211                 if (ss != null) {
1212                     toastText = mContext.getString(
1213                             R.string.volume_dialog_ringer_guidance_ring,
1214                             Utils.formatPercentage(ss.level, ss.levelMax));
1215                 }
1216                 break;
1217             case RINGER_MODE_SILENT:
1218                 toastText = mContext.getString(
1219                         com.android.internal.R.string.volume_dialog_ringer_guidance_silent);
1220                 break;
1221             case RINGER_MODE_VIBRATE:
1222             default:
1223                 toastText = mContext.getString(
1224                         com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate);
1225         }
1226 
1227         Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT).show();
1228         seenToastCount++;
1229         Prefs.putInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, seenToastCount);
1230     }
1231 
show(int reason)1232     public void show(int reason) {
1233         mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget();
1234     }
1235 
dismiss(int reason)1236     public void dismiss(int reason) {
1237         mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
1238     }
1239 
showH(int reason)1240     private void showH(int reason) {
1241         if (D.BUG) Log.d(TAG, "showH r=" + Events.SHOW_REASONS[reason]);
1242         mHandler.removeMessages(H.SHOW);
1243         mHandler.removeMessages(H.DISMISS);
1244         rescheduleTimeoutH();
1245 
1246         if (mConfigChanged) {
1247             initDialog(); // resets mShowing to false
1248             mConfigurableTexts.update();
1249             mConfigChanged = false;
1250         }
1251 
1252         initSettingsH();
1253         mShowing = true;
1254         mIsAnimatingDismiss = false;
1255         mDialog.show();
1256         Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
1257         mController.notifyVisible(true);
1258         mController.getCaptionsComponentState(false);
1259         checkODICaptionsTooltip(false);
1260         updateBackgroundForDrawerClosedAmount();
1261     }
1262 
rescheduleTimeoutH()1263     protected void rescheduleTimeoutH() {
1264         mHandler.removeMessages(H.DISMISS);
1265         final int timeout = computeTimeoutH();
1266         mHandler.sendMessageDelayed(mHandler
1267                 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout);
1268         if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
1269         mController.userActivity();
1270     }
1271 
computeTimeoutH()1272     private int computeTimeoutH() {
1273         if (mHovering) {
1274             return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_HOVERING_TIMEOUT_MILLIS,
1275                     AccessibilityManager.FLAG_CONTENT_CONTROLS);
1276         }
1277         if (mSafetyWarning != null) {
1278             return mAccessibilityMgr.getRecommendedTimeoutMillis(
1279                     DIALOG_SAFETYWARNING_TIMEOUT_MILLIS,
1280                     AccessibilityManager.FLAG_CONTENT_TEXT
1281                             | AccessibilityManager.FLAG_CONTENT_CONTROLS);
1282         }
1283         if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) {
1284             return mAccessibilityMgr.getRecommendedTimeoutMillis(
1285                     DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS,
1286                     AccessibilityManager.FLAG_CONTENT_TEXT
1287                             | AccessibilityManager.FLAG_CONTENT_CONTROLS);
1288         }
1289         return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_TIMEOUT_MILLIS,
1290                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
1291     }
1292 
dismissH(int reason)1293     protected void dismissH(int reason) {
1294         if (D.BUG) {
1295             Log.d(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason]
1296                     + " from: " + Debug.getCaller());
1297         }
1298         mHandler.removeMessages(H.DISMISS);
1299         mHandler.removeMessages(H.SHOW);
1300         if (mIsAnimatingDismiss) {
1301             return;
1302         }
1303         mIsAnimatingDismiss = true;
1304         mDialogView.animate().cancel();
1305         if (mShowing) {
1306             mShowing = false;
1307             // Only logs when the volume dialog visibility is changed.
1308             Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason);
1309         }
1310         mDialogView.setTranslationX(0);
1311         mDialogView.setAlpha(1);
1312         ViewPropertyAnimator animator = mDialogView.animate()
1313                 .alpha(0)
1314                 .setDuration(mDialogHideAnimationDurationMs)
1315                 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
1316                 .withEndAction(() -> mHandler.postDelayed(() -> {
1317                     mDialog.dismiss();
1318                     tryToRemoveCaptionsTooltip();
1319                     mIsAnimatingDismiss = false;
1320 
1321                     hideRingerDrawer();
1322                 }, 50));
1323         if (!isLandscape()) animator.translationX(mDialogView.getWidth() / 2.0f);
1324         animator.start();
1325         checkODICaptionsTooltip(true);
1326         mController.notifyVisible(false);
1327         synchronized (mSafetyWarningLock) {
1328             if (mSafetyWarning != null) {
1329                 if (D.BUG) Log.d(TAG, "SafetyWarning dismissed");
1330                 mSafetyWarning.dismiss();
1331             }
1332         }
1333     }
1334 
showActiveStreamOnly()1335     private boolean showActiveStreamOnly() {
1336         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
1337                 || mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION);
1338     }
1339 
shouldBeVisibleH(VolumeRow row, VolumeRow activeRow)1340     private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) {
1341         boolean isActive = row.stream == activeRow.stream;
1342 
1343         if (isActive) {
1344             return true;
1345         }
1346 
1347         if (!mShowActiveStreamOnly) {
1348             if (row.stream == AudioSystem.STREAM_ACCESSIBILITY) {
1349                 return mShowA11yStream;
1350             }
1351 
1352             // if the active row is accessibility, then continue to display previous
1353             // active row since accessibility is displayed under it
1354             if (activeRow.stream == AudioSystem.STREAM_ACCESSIBILITY &&
1355                     row.stream == mPrevActiveStream) {
1356                 return true;
1357             }
1358 
1359             if (row.defaultStream) {
1360                 return activeRow.stream == STREAM_RING
1361                         || activeRow.stream == STREAM_ALARM
1362                         || activeRow.stream == STREAM_VOICE_CALL
1363                         || activeRow.stream == STREAM_ACCESSIBILITY
1364                         || mDynamic.get(activeRow.stream);
1365             }
1366         }
1367 
1368         return false;
1369     }
1370 
updateRowsH(final VolumeRow activeRow)1371     private void updateRowsH(final VolumeRow activeRow) {
1372         if (D.BUG) Log.d(TAG, "updateRowsH");
1373         if (!mShowing) {
1374             trimObsoleteH();
1375         }
1376 
1377         // Index of the last row that is actually visible.
1378         int rightmostVisibleRowIndex = !isRtl() ? -1 : Short.MAX_VALUE;
1379 
1380         // apply changes to all rows
1381         for (final VolumeRow row : mRows) {
1382             final boolean isActive = row == activeRow;
1383             final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow);
1384             Util.setVisOrGone(row.view, shouldBeVisible);
1385 
1386             if (shouldBeVisible && mRingerAndDrawerContainerBackground != null) {
1387                 // For RTL, the rightmost row has the lowest index since child views are laid out
1388                 // from right to left.
1389                 rightmostVisibleRowIndex =
1390                         !isRtl()
1391                                 ? Math.max(rightmostVisibleRowIndex,
1392                                 mDialogRowsView.indexOfChild(row.view))
1393                                 : Math.min(rightmostVisibleRowIndex,
1394                                         mDialogRowsView.indexOfChild(row.view));
1395 
1396                 // Add spacing between each of the visible rows - we'll remove the spacing from the
1397                 // last row after the loop.
1398                 final ViewGroup.LayoutParams layoutParams = row.view.getLayoutParams();
1399                 if (layoutParams instanceof LinearLayout.LayoutParams) {
1400                     final LinearLayout.LayoutParams linearLayoutParams =
1401                             ((LinearLayout.LayoutParams) layoutParams);
1402                     if (!isRtl()) {
1403                         linearLayoutParams.setMarginEnd(mRingerRowsPadding);
1404                     } else {
1405                         linearLayoutParams.setMarginStart(mRingerRowsPadding);
1406                     }
1407                 }
1408 
1409                 // Set the background on each of the rows. We'll remove this from the last row after
1410                 // the loop, since the last row's background is drawn by the main volume container.
1411                 row.view.setBackgroundDrawable(
1412                         mContext.getDrawable(R.drawable.volume_row_rounded_background));
1413             }
1414 
1415             if (row.view.isShown()) {
1416                 updateVolumeRowTintH(row, isActive);
1417             }
1418         }
1419 
1420         if (rightmostVisibleRowIndex > -1 && rightmostVisibleRowIndex < Short.MAX_VALUE) {
1421             final View lastVisibleChild = mDialogRowsView.getChildAt(rightmostVisibleRowIndex);
1422             final ViewGroup.LayoutParams layoutParams = lastVisibleChild.getLayoutParams();
1423             // Remove the spacing on the last row, and remove its background since the container is
1424             // drawing a background for this row.
1425             if (layoutParams instanceof LinearLayout.LayoutParams) {
1426                 final LinearLayout.LayoutParams linearLayoutParams =
1427                         ((LinearLayout.LayoutParams) layoutParams);
1428                 linearLayoutParams.setMarginStart(0);
1429                 linearLayoutParams.setMarginEnd(0);
1430                 lastVisibleChild.setBackgroundColor(Color.TRANSPARENT);
1431             }
1432         }
1433 
1434         updateBackgroundForDrawerClosedAmount();
1435     }
1436 
updateRingerH()1437     protected void updateRingerH() {
1438         if (mRinger != null && mState != null) {
1439             final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
1440             if (ss == null) {
1441                 return;
1442             }
1443 
1444             boolean isZenMuted = mState.zenMode == Global.ZEN_MODE_ALARMS
1445                     || mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
1446                     || (mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
1447                         && mState.disallowRinger);
1448             enableRingerViewsH(!isZenMuted);
1449             switch (mState.ringerModeInternal) {
1450                 case AudioManager.RINGER_MODE_VIBRATE:
1451                     mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
1452                     mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
1453                     addAccessibilityDescription(mRingerIcon, RINGER_MODE_VIBRATE,
1454                             mContext.getString(R.string.volume_ringer_hint_mute));
1455                     mRingerIcon.setTag(Events.ICON_STATE_VIBRATE);
1456                     break;
1457                 case AudioManager.RINGER_MODE_SILENT:
1458                     mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
1459                     mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
1460                     mRingerIcon.setTag(Events.ICON_STATE_MUTE);
1461                     addAccessibilityDescription(mRingerIcon, RINGER_MODE_SILENT,
1462                             mContext.getString(R.string.volume_ringer_hint_unmute));
1463                     break;
1464                 case AudioManager.RINGER_MODE_NORMAL:
1465                 default:
1466                     boolean muted = (mAutomute && ss.level == 0) || ss.muted;
1467                     if (!isZenMuted && muted) {
1468                         mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
1469                         mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
1470                         addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL,
1471                                 mContext.getString(R.string.volume_ringer_hint_unmute));
1472                         mRingerIcon.setTag(Events.ICON_STATE_MUTE);
1473                     } else {
1474                         mRingerIcon.setImageResource(R.drawable.ic_volume_ringer);
1475                         mSelectedRingerIcon.setImageResource(R.drawable.ic_volume_ringer);
1476                         if (mController.hasVibrator()) {
1477                             addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL,
1478                                     mContext.getString(R.string.volume_ringer_hint_vibrate));
1479                         } else {
1480                             addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL,
1481                                     mContext.getString(R.string.volume_ringer_hint_mute));
1482                         }
1483                         mRingerIcon.setTag(Events.ICON_STATE_UNMUTE);
1484                     }
1485                     break;
1486             }
1487         }
1488     }
1489 
addAccessibilityDescription(View view, int currState, String hintLabel)1490     private void addAccessibilityDescription(View view, int currState, String hintLabel) {
1491         view.setContentDescription(
1492                 mContext.getString(getStringDescriptionResourceForRingerMode(currState)));
1493         view.setAccessibilityDelegate(new AccessibilityDelegate() {
1494             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
1495                 super.onInitializeAccessibilityNodeInfo(host, info);
1496                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
1497                                 AccessibilityNodeInfo.ACTION_CLICK, hintLabel));
1498             }
1499         });
1500     }
1501 
getStringDescriptionResourceForRingerMode(int mode)1502     private int getStringDescriptionResourceForRingerMode(int mode) {
1503         switch (mode) {
1504             case RINGER_MODE_SILENT:
1505                 return R.string.volume_ringer_status_silent;
1506             case RINGER_MODE_VIBRATE:
1507                 return R.string.volume_ringer_status_vibrate;
1508             case RINGER_MODE_NORMAL:
1509             default:
1510                 return R.string.volume_ringer_status_normal;
1511         }
1512     }
1513 
1514     /**
1515      * Toggles enable state of views in a VolumeRow (not including seekbar or icon)
1516      * Hides/shows zen icon
1517      * @param enable whether to enable volume row views and hide dnd icon
1518      */
enableVolumeRowViewsH(VolumeRow row, boolean enable)1519     private void enableVolumeRowViewsH(VolumeRow row, boolean enable) {
1520         boolean showDndIcon = !enable;
1521         row.dndIcon.setVisibility(showDndIcon ? VISIBLE : GONE);
1522     }
1523 
1524     /**
1525      * Toggles enable state of footer/ringer views
1526      * Hides/shows zen icon
1527      * @param enable whether to enable ringer views and hide dnd icon
1528      */
enableRingerViewsH(boolean enable)1529     private void enableRingerViewsH(boolean enable) {
1530         if (mRingerIcon != null) {
1531             mRingerIcon.setEnabled(enable);
1532         }
1533         if (mZenIcon != null) {
1534             mZenIcon.setVisibility(enable ? GONE : VISIBLE);
1535         }
1536     }
1537 
trimObsoleteH()1538     private void trimObsoleteH() {
1539         if (D.BUG) Log.d(TAG, "trimObsoleteH");
1540         for (int i = mRows.size() - 1; i >= 0; i--) {
1541             final VolumeRow row = mRows.get(i);
1542             if (row.ss == null || !row.ss.dynamic) continue;
1543             if (!mDynamic.get(row.stream)) {
1544                 mRows.remove(i);
1545                 mDialogRowsView.removeView(row.view);
1546                 mConfigurableTexts.remove(row.header);
1547             }
1548         }
1549     }
1550 
onStateChangedH(State state)1551     protected void onStateChangedH(State state) {
1552         if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString());
1553         if (mState != null && state != null
1554                 && mState.ringerModeInternal != -1
1555                 && mState.ringerModeInternal != state.ringerModeInternal
1556                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
1557             mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK));
1558         }
1559 
1560         mState = state;
1561         mDynamic.clear();
1562         // add any new dynamic rows
1563         for (int i = 0; i < state.states.size(); i++) {
1564             final int stream = state.states.keyAt(i);
1565             final StreamState ss = state.states.valueAt(i);
1566             if (!ss.dynamic) continue;
1567             mDynamic.put(stream, true);
1568             if (findRow(stream) == null) {
1569                 addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true,
1570                         false, true);
1571             }
1572         }
1573 
1574         if (mActiveStream != state.activeStream) {
1575             mPrevActiveStream = mActiveStream;
1576             mActiveStream = state.activeStream;
1577             VolumeRow activeRow = getActiveRow();
1578             updateRowsH(activeRow);
1579             if (mShowing) rescheduleTimeoutH();
1580         }
1581         for (VolumeRow row : mRows) {
1582             updateVolumeRowH(row);
1583         }
1584         updateRingerH();
1585         mWindow.setTitle(composeWindowTitle());
1586     }
1587 
composeWindowTitle()1588     CharSequence composeWindowTitle() {
1589         return mContext.getString(R.string.volume_dialog_title, getStreamLabelH(getActiveRow().ss));
1590     }
1591 
updateVolumeRowH(VolumeRow row)1592     private void updateVolumeRowH(VolumeRow row) {
1593         if (D.BUG) Log.i(TAG, "updateVolumeRowH s=" + row.stream);
1594         if (mState == null) return;
1595         final StreamState ss = mState.states.get(row.stream);
1596         if (ss == null) return;
1597         row.ss = ss;
1598         if (ss.level > 0) {
1599             row.lastAudibleLevel = ss.level;
1600         }
1601         if (ss.level == row.requestedLevel) {
1602             row.requestedLevel = -1;
1603         }
1604         final boolean isA11yStream = row.stream == STREAM_ACCESSIBILITY;
1605         final boolean isRingStream = row.stream == AudioManager.STREAM_RING;
1606         final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM;
1607         final boolean isAlarmStream = row.stream == STREAM_ALARM;
1608         final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC;
1609         final boolean isRingVibrate = isRingStream
1610                 && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE;
1611         final boolean isRingSilent = isRingStream
1612                 && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT;
1613         final boolean isZenPriorityOnly = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
1614         final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS;
1615         final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
1616         final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream)
1617                 : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream)
1618                 : isZenPriorityOnly ? ((isAlarmStream && mState.disallowAlarms) ||
1619                         (isMusicStream && mState.disallowMedia) ||
1620                         (isRingStream && mState.disallowRinger) ||
1621                         (isSystemStream && mState.disallowSystem))
1622                 : false;
1623 
1624         // update slider max
1625         final int max = ss.levelMax * 100;
1626         if (max != row.slider.getMax()) {
1627             row.slider.setMax(max);
1628         }
1629         // update slider min
1630         final int min = ss.levelMin * 100;
1631         if (min != row.slider.getMin()) {
1632             row.slider.setMin(min);
1633         }
1634 
1635         // update header text
1636         Util.setText(row.header, getStreamLabelH(ss));
1637         row.slider.setContentDescription(row.header.getText());
1638         mConfigurableTexts.add(row.header, ss.name);
1639 
1640         // update icon
1641         final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted;
1642         final int iconRes;
1643         if (isRingVibrate) {
1644             iconRes = R.drawable.ic_volume_ringer_vibrate;
1645         } else if (isRingSilent || zenMuted) {
1646             iconRes = row.iconMuteRes;
1647         } else if (ss.routedToBluetooth) {
1648             iconRes = isStreamMuted(ss) ? R.drawable.ic_volume_media_bt_mute
1649                                         : R.drawable.ic_volume_media_bt;
1650         } else if (isStreamMuted(ss)) {
1651             iconRes = ss.muted ? R.drawable.ic_volume_media_off : row.iconMuteRes;
1652         } else {
1653             iconRes = mShowLowMediaVolumeIcon && ss.level * 2 < (ss.levelMax + ss.levelMin)
1654                       ? R.drawable.ic_volume_media_low : row.iconRes;
1655         }
1656 
1657         row.setIcon(iconRes, mContext.getTheme());
1658         row.iconState =
1659                 iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE
1660                 : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes)
1661                         ? Events.ICON_STATE_MUTE
1662                 : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes
1663                         || iconRes == R.drawable.ic_volume_media_low)
1664                         ? Events.ICON_STATE_UNMUTE
1665                 : Events.ICON_STATE_UNKNOWN;
1666 
1667         if (row.icon != null) {
1668             if (iconEnabled) {
1669                 if (isRingStream) {
1670                     if (isRingVibrate) {
1671                         row.icon.setContentDescription(mContext.getString(
1672                                 R.string.volume_stream_content_description_unmute,
1673                                 getStreamLabelH(ss)));
1674                     } else {
1675                         if (mController.hasVibrator()) {
1676                             row.icon.setContentDescription(mContext.getString(
1677                                     mShowA11yStream
1678                                             ? R.string.volume_stream_content_description_vibrate_a11y
1679                                             : R.string.volume_stream_content_description_vibrate,
1680                                     getStreamLabelH(ss)));
1681                         } else {
1682                             row.icon.setContentDescription(mContext.getString(
1683                                     mShowA11yStream
1684                                             ? R.string.volume_stream_content_description_mute_a11y
1685                                             : R.string.volume_stream_content_description_mute,
1686                                     getStreamLabelH(ss)));
1687                         }
1688                     }
1689                 } else if (isA11yStream) {
1690                     row.icon.setContentDescription(getStreamLabelH(ss));
1691                 } else {
1692                     if (ss.muted || mAutomute && ss.level == 0) {
1693                         row.icon.setContentDescription(mContext.getString(
1694                                 R.string.volume_stream_content_description_unmute,
1695                                 getStreamLabelH(ss)));
1696                     } else {
1697                         row.icon.setContentDescription(mContext.getString(
1698                                 mShowA11yStream
1699                                         ? R.string.volume_stream_content_description_mute_a11y
1700                                         : R.string.volume_stream_content_description_mute,
1701                                 getStreamLabelH(ss)));
1702                     }
1703                 }
1704             } else {
1705                 row.icon.setContentDescription(getStreamLabelH(ss));
1706             }
1707         }
1708 
1709         // ensure tracking is disabled if zenMuted
1710         if (zenMuted) {
1711             row.tracking = false;
1712         }
1713         enableVolumeRowViewsH(row, !zenMuted);
1714 
1715         // update slider
1716         final boolean enableSlider = !zenMuted;
1717         final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0
1718                 : row.ss.level;
1719         updateVolumeRowSliderH(row, enableSlider, vlevel);
1720         if (row.number != null) row.number.setText(Integer.toString(vlevel));
1721     }
1722 
1723     private boolean isStreamMuted(final StreamState streamState) {
1724         return (mAutomute && streamState.level == 0) || streamState.muted;
1725     }
1726 
1727     private void updateVolumeRowTintH(VolumeRow row, boolean isActive) {
1728         if (isActive) {
1729             row.slider.requestFocus();
1730         }
1731         boolean useActiveColoring = isActive && row.slider.isEnabled();
1732         if (!useActiveColoring && !mChangeVolumeRowTintWhenInactive) {
1733             return;
1734         }
1735         final ColorStateList colorTint = useActiveColoring
1736                 ? Utils.getColorAccent(mContext)
1737                 : Utils.getColorAttr(mContext, com.android.internal.R.attr.colorAccentSecondary);
1738         final int alpha = useActiveColoring
1739                 ? Color.alpha(colorTint.getDefaultColor())
1740                 : getAlphaAttr(android.R.attr.secondaryContentAlpha);
1741 
1742         final ColorStateList bgTint = Utils.getColorAttr(
1743                 mContext, android.R.attr.colorBackgroundFloating);
1744 
1745         final ColorStateList inverseTextTint = Utils.getColorAttr(
1746                 mContext, com.android.internal.R.attr.textColorOnAccent);
1747 
1748         row.sliderProgressSolid.setTintList(colorTint);
1749         if (row.sliderBgIcon != null) {
1750             row.sliderBgIcon.setTintList(colorTint);
1751         }
1752 
1753         if (row.sliderBgSolid != null) {
1754             row.sliderBgSolid.setTintList(bgTint);
1755         }
1756 
1757         if (row.sliderProgressIcon != null) {
1758             row.sliderProgressIcon.setTintList(bgTint);
1759         }
1760 
1761         if (row.icon != null) {
1762             row.icon.setImageTintList(inverseTextTint);
1763             row.icon.setImageAlpha(alpha);
1764         }
1765 
1766         if (row.number != null) {
1767             row.number.setTextColor(colorTint);
1768             row.number.setAlpha(alpha);
1769         }
1770     }
1771 
1772     private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) {
1773         row.slider.setEnabled(enable);
1774         updateVolumeRowTintH(row, row.stream == mActiveStream);
1775         if (row.tracking) {
1776             return;  // don't update if user is sliding
1777         }
1778         final int progress = row.slider.getProgress();
1779         final int level = getImpliedLevel(row.slider, progress);
1780         final boolean rowVisible = row.view.getVisibility() == VISIBLE;
1781         final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt)
1782                 < USER_ATTEMPT_GRACE_PERIOD;
1783         mHandler.removeMessages(H.RECHECK, row);
1784         if (mShowing && rowVisible && inGracePeriod) {
1785             if (D.BUG) Log.d(TAG, "inGracePeriod");
1786             mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row),
1787                     row.userAttempt + USER_ATTEMPT_GRACE_PERIOD);
1788             return;  // don't update if visible and in grace period
1789         }
1790         if (vlevel == level) {
1791             if (mShowing && rowVisible) {
1792                 return;  // don't clamp if visible
1793             }
1794         }
1795         final int newProgress = vlevel * 100;
1796         if (progress != newProgress) {
1797             if (mShowing && rowVisible) {
1798                 // animate!
1799                 if (row.anim != null && row.anim.isRunning()
1800                         && row.animTargetProgress == newProgress) {
1801                     return;  // already animating to the target progress
1802                 }
1803                 // start/update animation
1804                 if (row.anim == null) {
1805                     row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress);
1806                     row.anim.setInterpolator(new DecelerateInterpolator());
1807                 } else {
1808                     row.anim.cancel();
1809                     row.anim.setIntValues(progress, newProgress);
1810                 }
1811                 row.animTargetProgress = newProgress;
1812                 row.anim.setDuration(UPDATE_ANIMATION_DURATION);
1813                 row.anim.start();
1814             } else {
1815                 // update slider directly to clamped value
1816                 if (row.anim != null) {
1817                     row.anim.cancel();
1818                 }
1819                 row.slider.setProgress(newProgress, true);
1820             }
1821         }
1822     }
1823 
1824     private void recheckH(VolumeRow row) {
1825         if (row == null) {
1826             if (D.BUG) Log.d(TAG, "recheckH ALL");
1827             trimObsoleteH();
1828             for (VolumeRow r : mRows) {
1829                 updateVolumeRowH(r);
1830             }
1831         } else {
1832             if (D.BUG) Log.d(TAG, "recheckH " + row.stream);
1833             updateVolumeRowH(row);
1834         }
1835     }
1836 
1837     private void setStreamImportantH(int stream, boolean important) {
1838         for (VolumeRow row : mRows) {
1839             if (row.stream == stream) {
1840                 row.important = important;
1841                 return;
1842             }
1843         }
1844     }
1845 
1846     private void showSafetyWarningH(int flags) {
1847         if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0
1848                 || mShowing) {
1849             synchronized (mSafetyWarningLock) {
1850                 if (mSafetyWarning != null) {
1851                     return;
1852                 }
1853                 mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) {
1854                     @Override
1855                     protected void cleanUp() {
1856                         synchronized (mSafetyWarningLock) {
1857                             mSafetyWarning = null;
1858                         }
1859                         recheckH(null);
1860                     }
1861                 };
1862                 mSafetyWarning.show();
1863             }
1864             recheckH(null);
1865         }
1866         rescheduleTimeoutH();
1867     }
1868 
1869     private String getStreamLabelH(StreamState ss) {
1870         if (ss == null) {
1871             return "";
1872         }
1873         if (ss.remoteLabel != null) {
1874             return ss.remoteLabel;
1875         }
1876         try {
1877             return mContext.getResources().getString(ss.name);
1878         } catch (Resources.NotFoundException e) {
1879             Slog.e(TAG, "Can't find translation for stream " + ss);
1880             return "";
1881         }
1882     }
1883 
1884     private Runnable getSinglePressFor(ImageButton button) {
1885         return () -> {
1886             if (button != null) {
1887                 button.setPressed(true);
1888                 button.postOnAnimationDelayed(getSingleUnpressFor(button), 200);
1889             }
1890         };
1891     }
1892 
1893     private Runnable getSingleUnpressFor(ImageButton button) {
1894         return () -> {
1895             if (button != null) {
1896                 button.setPressed(false);
1897             }
1898         };
1899     }
1900 
1901     /**
1902      * Return the size of the 1-2 extra ringer options that are made visible when the ringer drawer
1903      * is opened. The drawer options are square so this can be used for height calculations (when in
1904      * portrait, and the drawer opens upward) or for width (when opening sideways in landscape).
1905      */
getRingerDrawerOpenExtraSize()1906     private int getRingerDrawerOpenExtraSize() {
1907         return (mRingerCount - 1) * mRingerDrawerItemSize;
1908     }
1909 
updateBackgroundForDrawerClosedAmount()1910     private void updateBackgroundForDrawerClosedAmount() {
1911         if (mRingerAndDrawerContainerBackground == null) {
1912             return;
1913         }
1914 
1915         final Rect bounds = mRingerAndDrawerContainerBackground.copyBounds();
1916         if (!isLandscape()) {
1917             bounds.top = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize());
1918         } else {
1919             bounds.left = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize());
1920         }
1921         mRingerAndDrawerContainerBackground.setBounds(bounds);
1922     }
1923 
1924     /*
1925      * The top container is responsible for drawing the solid color background behind the rightmost
1926      * (primary) volume row. This is because the volume drawer animates in from below, initially
1927      * overlapping the primary row. We need the drawer to draw below the row's SeekBar, since it
1928      * looks strange to overlap it, but above the row's background color, since otherwise it will be
1929      * clipped.
1930      *
1931      * Since we can't be both above and below the volume row view, we'll be below it, and render the
1932      * background color in the container since they're both above that.
1933      */
setTopContainerBackgroundDrawable()1934     private void setTopContainerBackgroundDrawable() {
1935         if (mTopContainer == null) {
1936             return;
1937         }
1938 
1939         final ColorDrawable solidDrawable = new ColorDrawable(
1940                 Utils.getColorAttrDefaultColor(mContext, com.android.internal.R.attr.colorSurface));
1941 
1942         final LayerDrawable background = new LayerDrawable(new Drawable[] { solidDrawable });
1943 
1944         // Size the solid color to match the primary volume row. In landscape, extend it upwards
1945         // slightly so that it fills in the bottom corners of the ringer icon, whose background is
1946         // rounded on all sides so that it can expand to the left, outside the dialog's background.
1947         background.setLayerSize(0, mDialogWidth,
1948                 !isLandscape()
1949                         ? mDialogRowsView.getHeight()
1950                         : mDialogRowsView.getHeight() + mDialogCornerRadius);
1951         // Inset the top so that the color only renders below the ringer drawer, which has its own
1952         // background. In landscape, reduce the inset slightly since we are using the background to
1953         // fill in the corners of the closed ringer drawer.
1954         background.setLayerInsetTop(0,
1955                 !isLandscape()
1956                         ? mDialogRowsViewContainer.getTop()
1957                         : mDialogRowsViewContainer.getTop() - mDialogCornerRadius);
1958 
1959         // Set gravity to top-right, since additional rows will be added on the left.
1960         background.setLayerGravity(0, Gravity.TOP | Gravity.RIGHT);
1961 
1962         // In landscape, the ringer drawer animates out to the left (instead of down). Since the
1963         // drawer comes from the right (beyond the bounds of the dialog), we should clip it so it
1964         // doesn't draw outside the dialog background. This isn't an issue in portrait, since the
1965         // drawer animates downward, below the volume row.
1966         if (isLandscape()) {
1967             mRingerAndDrawerContainer.setOutlineProvider(new ViewOutlineProvider() {
1968                 @Override
1969                 public void getOutline(View view, Outline outline) {
1970                     outline.setRoundRect(
1971                             0, 0, view.getWidth(), view.getHeight(), mDialogCornerRadius);
1972                 }
1973             });
1974             mRingerAndDrawerContainer.setClipToOutline(true);
1975         }
1976 
1977         mTopContainer.setBackground(background);
1978     }
1979 
1980     private final VolumeDialogController.Callbacks mControllerCallbackH
1981             = new VolumeDialogController.Callbacks() {
1982         @Override
1983         public void onShowRequested(int reason) {
1984             showH(reason);
1985         }
1986 
1987         @Override
1988         public void onDismissRequested(int reason) {
1989             dismissH(reason);
1990         }
1991 
1992         @Override
1993         public void onScreenOff() {
1994             dismissH(Events.DISMISS_REASON_SCREEN_OFF);
1995         }
1996 
1997         @Override
1998         public void onStateChanged(State state) {
1999             onStateChangedH(state);
2000         }
2001 
2002         @Override
2003         public void onLayoutDirectionChanged(int layoutDirection) {
2004             mDialogView.setLayoutDirection(layoutDirection);
2005         }
2006 
2007         @Override
2008         public void onConfigurationChanged() {
2009             mDialog.dismiss();
2010             mConfigChanged = true;
2011         }
2012 
2013         @Override
2014         public void onShowVibrateHint() {
2015             if (mSilentMode) {
2016                 mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false);
2017             }
2018         }
2019 
2020         @Override
2021         public void onShowSilentHint() {
2022             if (mSilentMode) {
2023                 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
2024             }
2025         }
2026 
2027         @Override
2028         public void onShowSafetyWarning(int flags) {
2029             showSafetyWarningH(flags);
2030         }
2031 
2032         @Override
2033         public void onAccessibilityModeChanged(Boolean showA11yStream) {
2034             mShowA11yStream = showA11yStream == null ? false : showA11yStream;
2035             VolumeRow activeRow = getActiveRow();
2036             if (!mShowA11yStream && STREAM_ACCESSIBILITY == activeRow.stream) {
2037                 dismissH(Events.DISMISS_STREAM_GONE);
2038             } else {
2039                 updateRowsH(activeRow);
2040             }
2041 
2042         }
2043 
2044         @Override
2045         public void onCaptionComponentStateChanged(
2046                 Boolean isComponentEnabled, Boolean fromTooltip) {
2047             updateODICaptionsH(isComponentEnabled, fromTooltip);
2048         }
2049     };
2050 
2051     private final class H extends Handler {
2052         private static final int SHOW = 1;
2053         private static final int DISMISS = 2;
2054         private static final int RECHECK = 3;
2055         private static final int RECHECK_ALL = 4;
2056         private static final int SET_STREAM_IMPORTANT = 5;
2057         private static final int RESCHEDULE_TIMEOUT = 6;
2058         private static final int STATE_CHANGED = 7;
2059 
H()2060         public H() {
2061             super(Looper.getMainLooper());
2062         }
2063 
2064         @Override
handleMessage(Message msg)2065         public void handleMessage(Message msg) {
2066             switch (msg.what) {
2067                 case SHOW: showH(msg.arg1); break;
2068                 case DISMISS: dismissH(msg.arg1); break;
2069                 case RECHECK: recheckH((VolumeRow) msg.obj); break;
2070                 case RECHECK_ALL: recheckH(null); break;
2071                 case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break;
2072                 case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break;
2073                 case STATE_CHANGED: onStateChangedH(mState); break;
2074             }
2075         }
2076     }
2077 
2078     private final class CustomDialog extends Dialog implements DialogInterface {
CustomDialog(Context context)2079         public CustomDialog(Context context) {
2080             super(context, R.style.volume_dialog_theme);
2081         }
2082 
2083         /**
2084          * NOTE: This will only be called for touches within the touchable region of the volume
2085          * dialog, as returned by {@link #onComputeInternalInsets}. Other touches, even if they are
2086          * within the bounds of the volume dialog, will fall through to the window below.
2087          */
2088         @Override
dispatchTouchEvent(MotionEvent ev)2089         public boolean dispatchTouchEvent(MotionEvent ev) {
2090             rescheduleTimeoutH();
2091             return super.dispatchTouchEvent(ev);
2092         }
2093 
2094         @Override
onStart()2095         protected void onStart() {
2096             super.setCanceledOnTouchOutside(true);
2097             super.onStart();
2098         }
2099 
2100         @Override
onStop()2101         protected void onStop() {
2102             super.onStop();
2103             mHandler.sendEmptyMessage(H.RECHECK_ALL);
2104         }
2105 
2106         /**
2107          * NOTE: This will be called with ACTION_OUTSIDE MotionEvents for touches that occur outside
2108          * of the touchable region of the volume dialog (as returned by
2109          * {@link #onComputeInternalInsets}) even if those touches occurred within the bounds of the
2110          * volume dialog.
2111          */
2112         @Override
onTouchEvent(MotionEvent event)2113         public boolean onTouchEvent(MotionEvent event) {
2114             if (mShowing) {
2115                 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
2116                     dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
2117                     return true;
2118                 }
2119             }
2120             return false;
2121         }
2122     }
2123 
2124     private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
2125         private final VolumeRow mRow;
2126 
VolumeSeekBarChangeListener(VolumeRow row)2127         private VolumeSeekBarChangeListener(VolumeRow row) {
2128             mRow = row;
2129         }
2130 
2131         @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)2132         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
2133             if (mRow.ss == null) return;
2134             if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
2135                     + " onProgressChanged " + progress + " fromUser=" + fromUser);
2136             if (!fromUser) return;
2137             if (mRow.ss.levelMin > 0) {
2138                 final int minProgress = mRow.ss.levelMin * 100;
2139                 if (progress < minProgress) {
2140                     seekBar.setProgress(minProgress);
2141                     progress = minProgress;
2142                 }
2143             }
2144             final int userLevel = getImpliedLevel(seekBar, progress);
2145             if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) {
2146                 mRow.userAttempt = SystemClock.uptimeMillis();
2147                 if (mRow.requestedLevel != userLevel) {
2148                     mController.setActiveStream(mRow.stream);
2149                     mController.setStreamVolume(mRow.stream, userLevel);
2150                     mRow.requestedLevel = userLevel;
2151                     Events.writeEvent(Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream,
2152                             userLevel);
2153                 }
2154             }
2155         }
2156 
2157         @Override
onStartTrackingTouch(SeekBar seekBar)2158         public void onStartTrackingTouch(SeekBar seekBar) {
2159             if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream);
2160             mController.setActiveStream(mRow.stream);
2161             mRow.tracking = true;
2162         }
2163 
2164         @Override
onStopTrackingTouch(SeekBar seekBar)2165         public void onStopTrackingTouch(SeekBar seekBar) {
2166             if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream);
2167             mRow.tracking = false;
2168             mRow.userAttempt = SystemClock.uptimeMillis();
2169             final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress());
2170             Events.writeEvent(Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel);
2171             if (mRow.ss.level != userLevel) {
2172                 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow),
2173                         USER_ATTEMPT_GRACE_PERIOD);
2174             }
2175         }
2176     }
2177 
2178     private final class Accessibility extends AccessibilityDelegate {
init()2179         public void init() {
2180             mDialogView.setAccessibilityDelegate(this);
2181         }
2182 
2183         @Override
dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event)2184         public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
2185             // Activities populate their title here. Follow that example.
2186             event.getText().add(composeWindowTitle());
2187             return true;
2188         }
2189 
2190         @Override
onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event)2191         public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
2192                 AccessibilityEvent event) {
2193             rescheduleTimeoutH();
2194             return super.onRequestSendAccessibilityEvent(host, child, event);
2195         }
2196     }
2197 
2198     private static class VolumeRow {
2199         private View view;
2200         private TextView header;
2201         private ImageButton icon;
2202         private Drawable sliderBgSolid;
2203         private AlphaTintDrawableWrapper sliderBgIcon;
2204         private Drawable sliderProgressSolid;
2205         private AlphaTintDrawableWrapper sliderProgressIcon;
2206         private SeekBar slider;
2207         private TextView number;
2208         private int stream;
2209         private StreamState ss;
2210         private long userAttempt;  // last user-driven slider change
2211         private boolean tracking;  // tracking slider touch
2212         private int requestedLevel = -1;  // pending user-requested level via progress changed
2213         private int iconRes;
2214         private int iconMuteRes;
2215         private boolean important;
2216         private boolean defaultStream;
2217         private ColorStateList cachedTint;
2218         private int iconState;  // from Events
2219         private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
2220         private int animTargetProgress;
2221         private int lastAudibleLevel = 1;
2222         private FrameLayout dndIcon;
2223 
setIcon(int iconRes, Resources.Theme theme)2224         void setIcon(int iconRes, Resources.Theme theme) {
2225             if (icon != null) {
2226                 icon.setImageResource(iconRes);
2227             }
2228 
2229             if (sliderProgressIcon != null) {
2230                 sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
2231             }
2232             if (sliderBgIcon != null) {
2233                 sliderBgIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
2234             }
2235         }
2236     }
2237 
2238     /**
2239      * Click listener added to each ringer option in the drawer. This will initiate the animation to
2240      * select and then close the ringer drawer, and actually change the ringer mode.
2241      */
2242     private class RingerDrawerItemClickListener implements View.OnClickListener {
2243         private final int mClickedRingerMode;
2244 
RingerDrawerItemClickListener(int clickedRingerMode)2245         RingerDrawerItemClickListener(int clickedRingerMode) {
2246             mClickedRingerMode = clickedRingerMode;
2247         }
2248 
2249         @Override
onClick(View view)2250         public void onClick(View view) {
2251             // If the ringer drawer isn't open, don't let anything in it be clicked.
2252             if (!mIsRingerDrawerOpen) {
2253                 return;
2254             }
2255 
2256             setRingerMode(mClickedRingerMode);
2257 
2258             mRingerDrawerIconAnimatingSelected = getDrawerIconViewForMode(mClickedRingerMode);
2259             mRingerDrawerIconAnimatingDeselected = getDrawerIconViewForMode(
2260                     mState.ringerModeInternal);
2261 
2262             // Begin switching the selected icon and deselected icon colors since the background is
2263             // going to animate behind the new selection.
2264             mRingerDrawerIconColorAnimator.start();
2265 
2266             mSelectedRingerContainer.setVisibility(View.INVISIBLE);
2267             mRingerDrawerNewSelectionBg.setAlpha(1f);
2268             mRingerDrawerNewSelectionBg.animate()
2269                     .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
2270                     .setDuration(DRAWER_ANIMATION_DURATION_SHORT)
2271                     .withEndAction(() -> {
2272                         mRingerDrawerNewSelectionBg.setAlpha(0f);
2273 
2274                         if (!isLandscape()) {
2275                             mSelectedRingerContainer.setTranslationY(
2276                                     getTranslationInDrawerForRingerMode(mClickedRingerMode));
2277                         } else {
2278                             mSelectedRingerContainer.setTranslationX(
2279                                     getTranslationInDrawerForRingerMode(mClickedRingerMode));
2280                         }
2281 
2282                         mSelectedRingerContainer.setVisibility(VISIBLE);
2283                         hideRingerDrawer();
2284                     });
2285 
2286             if (!isLandscape()) {
2287                 mRingerDrawerNewSelectionBg.animate()
2288                         .translationY(getTranslationInDrawerForRingerMode(mClickedRingerMode))
2289                         .start();
2290             } else {
2291                 mRingerDrawerNewSelectionBg.animate()
2292                         .translationX(getTranslationInDrawerForRingerMode(mClickedRingerMode))
2293                         .start();
2294             }
2295         }
2296     }
2297 }
2298