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