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