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