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