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