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