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