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