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.VISIBLE; 32 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 33 34 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; 35 36 import android.animation.ObjectAnimator; 37 import android.annotation.SuppressLint; 38 import android.app.ActivityManager; 39 import android.app.Dialog; 40 import android.app.KeyguardManager; 41 import android.content.ContentResolver; 42 import android.content.Context; 43 import android.content.DialogInterface; 44 import android.content.Intent; 45 import android.content.pm.PackageManager; 46 import android.content.res.ColorStateList; 47 import android.content.res.Configuration; 48 import android.content.res.Resources; 49 import android.content.res.TypedArray; 50 import android.graphics.Color; 51 import android.graphics.PixelFormat; 52 import android.graphics.drawable.ColorDrawable; 53 import android.media.AudioManager; 54 import android.media.AudioSystem; 55 import android.os.Debug; 56 import android.os.Handler; 57 import android.os.Looper; 58 import android.os.Message; 59 import android.os.SystemClock; 60 import android.os.VibrationEffect; 61 import android.provider.Settings; 62 import android.provider.Settings.Global; 63 import android.text.InputFilter; 64 import android.util.Log; 65 import android.util.Slog; 66 import android.util.SparseBooleanArray; 67 import android.view.ContextThemeWrapper; 68 import android.view.MotionEvent; 69 import android.view.View; 70 import android.view.View.AccessibilityDelegate; 71 import android.view.ViewGroup; 72 import android.view.ViewPropertyAnimator; 73 import android.view.ViewStub; 74 import android.view.Window; 75 import android.view.WindowManager; 76 import android.view.accessibility.AccessibilityEvent; 77 import android.view.accessibility.AccessibilityManager; 78 import android.view.accessibility.AccessibilityNodeInfo; 79 import android.view.animation.DecelerateInterpolator; 80 import android.widget.FrameLayout; 81 import android.widget.ImageButton; 82 import android.widget.SeekBar; 83 import android.widget.SeekBar.OnSeekBarChangeListener; 84 import android.widget.TextView; 85 import android.widget.Toast; 86 87 import com.android.settingslib.Utils; 88 import com.android.systemui.Dependency; 89 import com.android.systemui.Prefs; 90 import com.android.systemui.R; 91 import com.android.systemui.media.dialog.MediaOutputDialogFactory; 92 import com.android.systemui.plugins.ActivityStarter; 93 import com.android.systemui.plugins.VolumeDialog; 94 import com.android.systemui.plugins.VolumeDialogController; 95 import com.android.systemui.plugins.VolumeDialogController.State; 96 import com.android.systemui.plugins.VolumeDialogController.StreamState; 97 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; 98 import com.android.systemui.statusbar.policy.ConfigurationController; 99 import com.android.systemui.statusbar.policy.DeviceProvisionedController; 100 101 import java.io.PrintWriter; 102 import java.util.ArrayList; 103 import java.util.List; 104 105 /** 106 * Visual presentation of the volume dialog. 107 * 108 * A client of VolumeDialogControllerImpl and its state model. 109 * 110 * Methods ending in "H" must be called on the (ui) handler. 111 */ 112 public class VolumeDialogImpl implements VolumeDialog, 113 ConfigurationController.ConfigurationListener { 114 private static final String TAG = Util.logTag(VolumeDialogImpl.class); 115 116 private static final long USER_ATTEMPT_GRACE_PERIOD = 1000; 117 private static final int UPDATE_ANIMATION_DURATION = 80; 118 119 static final int DIALOG_TIMEOUT_MILLIS = 3000; 120 static final int DIALOG_SAFETYWARNING_TIMEOUT_MILLIS = 5000; 121 static final int DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS = 5000; 122 static final int DIALOG_HOVERING_TIMEOUT_MILLIS = 16000; 123 static final int DIALOG_SHOW_ANIMATION_DURATION = 300; 124 static final int DIALOG_HIDE_ANIMATION_DURATION = 250; 125 126 private final Context mContext; 127 private final H mHandler = new H(); 128 private final VolumeDialogController mController; 129 private final DeviceProvisionedController mDeviceProvisionedController; 130 131 private Window mWindow; 132 private CustomDialog mDialog; 133 private ViewGroup mDialogView; 134 private ViewGroup mDialogRowsView; 135 private ViewGroup mRinger; 136 private ImageButton mRingerIcon; 137 private ViewGroup mODICaptionsView; 138 private CaptionsToggleImageButton mODICaptionsIcon; 139 private View mSettingsView; 140 private ImageButton mSettingsIcon; 141 private FrameLayout mZenIcon; 142 private final List<VolumeRow> mRows = new ArrayList<>(); 143 private ConfigurableTexts mConfigurableTexts; 144 private final SparseBooleanArray mDynamic = new SparseBooleanArray(); 145 private final KeyguardManager mKeyguard; 146 private final ActivityManager mActivityManager; 147 private final AccessibilityManagerWrapper mAccessibilityMgr; 148 private final Object mSafetyWarningLock = new Object(); 149 private final Accessibility mAccessibility = new Accessibility(); 150 151 private boolean mShowing; 152 private boolean mShowA11yStream; 153 154 private int mActiveStream; 155 private int mPrevActiveStream; 156 private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE; 157 private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE; 158 private State mState; 159 private SafetyWarningDialog mSafetyWarning; 160 private boolean mHovering = false; 161 private boolean mShowActiveStreamOnly; 162 private boolean mConfigChanged = false; 163 private boolean mIsAnimatingDismiss = false; 164 private boolean mHasSeenODICaptionsTooltip; 165 private ViewStub mODICaptionsTooltipViewStub; 166 private View mODICaptionsTooltipView = null; 167 VolumeDialogImpl(Context context)168 public VolumeDialogImpl(Context context) { 169 mContext = 170 new ContextThemeWrapper(context, R.style.qs_theme); 171 mController = Dependency.get(VolumeDialogController.class); 172 mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); 173 mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 174 mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class); 175 mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); 176 mShowActiveStreamOnly = showActiveStreamOnly(); 177 mHasSeenODICaptionsTooltip = 178 Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false); 179 } 180 181 @Override onUiModeChanged()182 public void onUiModeChanged() { 183 mContext.getTheme().applyStyle(mContext.getThemeResId(), true); 184 } 185 init(int windowType, Callback callback)186 public void init(int windowType, Callback callback) { 187 initDialog(); 188 189 mAccessibility.init(); 190 191 mController.addCallback(mControllerCallbackH, mHandler); 192 mController.getState(); 193 194 Dependency.get(ConfigurationController.class).addCallback(this); 195 } 196 197 @Override destroy()198 public void destroy() { 199 mController.removeCallback(mControllerCallbackH); 200 mHandler.removeCallbacksAndMessages(null); 201 Dependency.get(ConfigurationController.class).removeCallback(this); 202 } 203 initDialog()204 private void initDialog() { 205 mDialog = new CustomDialog(mContext); 206 207 mConfigurableTexts = new ConfigurableTexts(mContext); 208 mHovering = false; 209 mShowing = false; 210 mWindow = mDialog.getWindow(); 211 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 212 mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 213 mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND 214 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); 215 mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 216 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 217 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 218 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 219 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); 220 mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); 221 mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); 222 WindowManager.LayoutParams lp = mWindow.getAttributes(); 223 lp.format = PixelFormat.TRANSLUCENT; 224 lp.setTitle(VolumeDialogImpl.class.getSimpleName()); 225 lp.windowAnimations = -1; 226 lp.gravity = mContext.getResources().getInteger(R.integer.volume_dialog_gravity); 227 mWindow.setAttributes(lp); 228 mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT); 229 230 mDialog.setContentView(R.layout.volume_dialog); 231 mDialogView = mDialog.findViewById(R.id.volume_dialog); 232 mDialogView.setAlpha(0); 233 mDialog.setCanceledOnTouchOutside(true); 234 mDialog.setOnShowListener(dialog -> { 235 if (!isLandscape()) mDialogView.setTranslationX(mDialogView.getWidth() / 2.0f); 236 mDialogView.setAlpha(0); 237 mDialogView.animate() 238 .alpha(1) 239 .translationX(0) 240 .setDuration(DIALOG_SHOW_ANIMATION_DURATION) 241 .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator()) 242 .withEndAction(() -> { 243 if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) { 244 if (mRingerIcon != null) { 245 mRingerIcon.postOnAnimationDelayed( 246 getSinglePressFor(mRingerIcon), 1500); 247 } 248 } 249 }) 250 .start(); 251 }); 252 253 mDialogView.setOnHoverListener((v, event) -> { 254 int action = event.getActionMasked(); 255 mHovering = (action == MotionEvent.ACTION_HOVER_ENTER) 256 || (action == MotionEvent.ACTION_HOVER_MOVE); 257 rescheduleTimeoutH(); 258 return true; 259 }); 260 261 mDialogRowsView = mDialog.findViewById(R.id.volume_dialog_rows); 262 mRinger = mDialog.findViewById(R.id.ringer); 263 if (mRinger != null) { 264 mRingerIcon = mRinger.findViewById(R.id.ringer_icon); 265 mZenIcon = mRinger.findViewById(R.id.dnd_icon); 266 } 267 268 mODICaptionsView = mDialog.findViewById(R.id.odi_captions); 269 if (mODICaptionsView != null) { 270 mODICaptionsIcon = mODICaptionsView.findViewById(R.id.odi_captions_icon); 271 } 272 mODICaptionsTooltipViewStub = mDialog.findViewById(R.id.odi_captions_tooltip_stub); 273 if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipViewStub != null) { 274 mDialogView.removeView(mODICaptionsTooltipViewStub); 275 mODICaptionsTooltipViewStub = null; 276 } 277 278 mSettingsView = mDialog.findViewById(R.id.settings_container); 279 mSettingsIcon = mDialog.findViewById(R.id.settings); 280 281 if (mRows.isEmpty()) { 282 if (!AudioSystem.isSingleVolume(mContext)) { 283 addRow(STREAM_ACCESSIBILITY, R.drawable.ic_volume_accessibility, 284 R.drawable.ic_volume_accessibility, true, false); 285 } 286 addRow(AudioManager.STREAM_MUSIC, 287 R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true); 288 if (!AudioSystem.isSingleVolume(mContext)) { 289 addRow(AudioManager.STREAM_RING, 290 R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true, false); 291 addRow(STREAM_ALARM, 292 R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false); 293 addRow(AudioManager.STREAM_VOICE_CALL, 294 com.android.internal.R.drawable.ic_phone, 295 com.android.internal.R.drawable.ic_phone, false, false); 296 addRow(AudioManager.STREAM_BLUETOOTH_SCO, 297 R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false, false); 298 addRow(AudioManager.STREAM_SYSTEM, R.drawable.ic_volume_system, 299 R.drawable.ic_volume_system_mute, false, false); 300 } 301 } else { 302 addExistingRows(); 303 } 304 305 updateRowsH(getActiveRow()); 306 initRingerH(); 307 initSettingsH(); 308 initODICaptionsH(); 309 } 310 getDialogView()311 protected ViewGroup getDialogView() { 312 return mDialogView; 313 } 314 getAlphaAttr(int attr)315 private int getAlphaAttr(int attr) { 316 TypedArray ta = mContext.obtainStyledAttributes(new int[]{attr}); 317 float alpha = ta.getFloat(0, 0); 318 ta.recycle(); 319 return (int) (alpha * 255); 320 } 321 isLandscape()322 private boolean isLandscape() { 323 return mContext.getResources().getConfiguration().orientation == 324 Configuration.ORIENTATION_LANDSCAPE; 325 } 326 setStreamImportant(int stream, boolean important)327 public void setStreamImportant(int stream, boolean important) { 328 mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget(); 329 } 330 setAutomute(boolean automute)331 public void setAutomute(boolean automute) { 332 if (mAutomute == automute) return; 333 mAutomute = automute; 334 mHandler.sendEmptyMessage(H.RECHECK_ALL); 335 } 336 setSilentMode(boolean silentMode)337 public void setSilentMode(boolean silentMode) { 338 if (mSilentMode == silentMode) return; 339 mSilentMode = silentMode; 340 mHandler.sendEmptyMessage(H.RECHECK_ALL); 341 } 342 addRow(int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream)343 private void addRow(int stream, int iconRes, int iconMuteRes, boolean important, 344 boolean defaultStream) { 345 addRow(stream, iconRes, iconMuteRes, important, defaultStream, false); 346 } 347 addRow(int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream, boolean dynamic)348 private void addRow(int stream, int iconRes, int iconMuteRes, boolean important, 349 boolean defaultStream, boolean dynamic) { 350 if (D.BUG) Slog.d(TAG, "Adding row for stream " + stream); 351 VolumeRow row = new VolumeRow(); 352 initRow(row, stream, iconRes, iconMuteRes, important, defaultStream); 353 mDialogRowsView.addView(row.view); 354 mRows.add(row); 355 } 356 addExistingRows()357 private void addExistingRows() { 358 int N = mRows.size(); 359 for (int i = 0; i < N; i++) { 360 final VolumeRow row = mRows.get(i); 361 initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important, 362 row.defaultStream); 363 mDialogRowsView.addView(row.view); 364 updateVolumeRowH(row); 365 } 366 } 367 getActiveRow()368 private VolumeRow getActiveRow() { 369 for (VolumeRow row : mRows) { 370 if (row.stream == mActiveStream) { 371 return row; 372 } 373 } 374 for (VolumeRow row : mRows) { 375 if (row.stream == STREAM_MUSIC) { 376 return row; 377 } 378 } 379 return mRows.get(0); 380 } 381 findRow(int stream)382 private VolumeRow findRow(int stream) { 383 for (VolumeRow row : mRows) { 384 if (row.stream == stream) return row; 385 } 386 return null; 387 } 388 dump(PrintWriter writer)389 public void dump(PrintWriter writer) { 390 writer.println(VolumeDialogImpl.class.getSimpleName() + " state:"); 391 writer.print(" mShowing: "); writer.println(mShowing); 392 writer.print(" mActiveStream: "); writer.println(mActiveStream); 393 writer.print(" mDynamic: "); writer.println(mDynamic); 394 writer.print(" mAutomute: "); writer.println(mAutomute); 395 writer.print(" mSilentMode: "); writer.println(mSilentMode); 396 } 397 getImpliedLevel(SeekBar seekBar, int progress)398 private static int getImpliedLevel(SeekBar seekBar, int progress) { 399 final int m = seekBar.getMax(); 400 final int n = m / 100 - 1; 401 final int level = progress == 0 ? 0 402 : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n)); 403 return level; 404 } 405 406 @SuppressLint("InflateParams") initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes, boolean important, boolean defaultStream)407 private void initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes, 408 boolean important, boolean defaultStream) { 409 row.stream = stream; 410 row.iconRes = iconRes; 411 row.iconMuteRes = iconMuteRes; 412 row.important = important; 413 row.defaultStream = defaultStream; 414 row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null); 415 row.view.setId(row.stream); 416 row.view.setTag(row); 417 row.header = row.view.findViewById(R.id.volume_row_header); 418 row.header.setId(20 * row.stream); 419 if (stream == STREAM_ACCESSIBILITY) { 420 row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); 421 } 422 row.dndIcon = row.view.findViewById(R.id.dnd_icon); 423 row.slider = row.view.findViewById(R.id.volume_row_slider); 424 row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); 425 426 row.anim = null; 427 428 row.icon = row.view.findViewById(R.id.volume_row_icon); 429 row.icon.setImageResource(iconRes); 430 if (row.stream != AudioSystem.STREAM_ACCESSIBILITY) { 431 row.icon.setOnClickListener(v -> { 432 Events.writeEvent(Events.EVENT_ICON_CLICK, row.stream, row.iconState); 433 mController.setActiveStream(row.stream); 434 if (row.stream == AudioManager.STREAM_RING) { 435 final boolean hasVibrator = mController.hasVibrator(); 436 if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { 437 if (hasVibrator) { 438 mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); 439 } else { 440 final boolean wasZero = row.ss.level == 0; 441 mController.setStreamVolume(stream, 442 wasZero ? row.lastAudibleLevel : 0); 443 } 444 } else { 445 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); 446 if (row.ss.level == 0) { 447 mController.setStreamVolume(stream, 1); 448 } 449 } 450 } else { 451 final boolean vmute = row.ss.level == row.ss.levelMin; 452 mController.setStreamVolume(stream, 453 vmute ? row.lastAudibleLevel : row.ss.levelMin); 454 } 455 row.userAttempt = 0; // reset the grace period, slider updates immediately 456 }); 457 } else { 458 row.icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 459 } 460 } 461 initSettingsH()462 public void initSettingsH() { 463 if (mSettingsView != null) { 464 mSettingsView.setVisibility( 465 mDeviceProvisionedController.isCurrentUserSetup() && 466 mActivityManager.getLockTaskModeState() == LOCK_TASK_MODE_NONE ? 467 VISIBLE : GONE); 468 } 469 if (mSettingsIcon != null) { 470 mSettingsIcon.setOnClickListener(v -> { 471 Events.writeEvent(Events.EVENT_SETTINGS_CLICK); 472 Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); 473 dismissH(DISMISS_REASON_SETTINGS_CLICKED); 474 Dependency.get(MediaOutputDialogFactory.class).dismiss(); 475 Dependency.get(ActivityStarter.class).startActivity(intent, 476 true /* dismissShade */); 477 }); 478 } 479 } 480 initRingerH()481 public void initRingerH() { 482 if (mRingerIcon != null) { 483 mRingerIcon.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE); 484 mRingerIcon.setOnClickListener(v -> { 485 Prefs.putBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true); 486 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 487 if (ss == null) { 488 return; 489 } 490 // normal -> vibrate -> silent -> normal (skip vibrate if device doesn't have 491 // a vibrator. 492 int newRingerMode; 493 final boolean hasVibrator = mController.hasVibrator(); 494 if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { 495 if (hasVibrator) { 496 newRingerMode = AudioManager.RINGER_MODE_VIBRATE; 497 } else { 498 newRingerMode = AudioManager.RINGER_MODE_SILENT; 499 } 500 } else if (mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { 501 newRingerMode = AudioManager.RINGER_MODE_SILENT; 502 } else { 503 newRingerMode = AudioManager.RINGER_MODE_NORMAL; 504 if (ss.level == 0) { 505 mController.setStreamVolume(AudioManager.STREAM_RING, 1); 506 } 507 } 508 Events.writeEvent(Events.EVENT_RINGER_TOGGLE, newRingerMode); 509 incrementManualToggleCount(); 510 updateRingerH(); 511 provideTouchFeedbackH(newRingerMode); 512 mController.setRingerMode(newRingerMode, false); 513 maybeShowToastH(newRingerMode); 514 }); 515 } 516 updateRingerH(); 517 } 518 initODICaptionsH()519 private void initODICaptionsH() { 520 if (mODICaptionsIcon != null) { 521 mODICaptionsIcon.setOnConfirmedTapListener(() -> { 522 onCaptionIconClicked(); 523 Events.writeEvent(Events.EVENT_ODI_CAPTIONS_CLICK); 524 }, mHandler); 525 } 526 527 mController.getCaptionsComponentState(false); 528 } 529 checkODICaptionsTooltip(boolean fromDismiss)530 private void checkODICaptionsTooltip(boolean fromDismiss) { 531 if (!mHasSeenODICaptionsTooltip && !fromDismiss && mODICaptionsTooltipViewStub != null) { 532 mController.getCaptionsComponentState(true); 533 } else { 534 if (mHasSeenODICaptionsTooltip && fromDismiss && mODICaptionsTooltipView != null) { 535 hideCaptionsTooltip(); 536 } 537 } 538 } 539 showCaptionsTooltip()540 protected void showCaptionsTooltip() { 541 if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipViewStub != null) { 542 mODICaptionsTooltipView = mODICaptionsTooltipViewStub.inflate(); 543 mODICaptionsTooltipView.findViewById(R.id.dismiss).setOnClickListener(v -> { 544 hideCaptionsTooltip(); 545 Events.writeEvent(Events.EVENT_ODI_CAPTIONS_TOOLTIP_CLICK); 546 }); 547 mODICaptionsTooltipViewStub = null; 548 rescheduleTimeoutH(); 549 } 550 551 if (mODICaptionsTooltipView != null) { 552 mODICaptionsTooltipView.setAlpha(0.f); 553 mODICaptionsTooltipView.animate() 554 .alpha(1.f) 555 .setStartDelay(DIALOG_SHOW_ANIMATION_DURATION) 556 .withEndAction(() -> { 557 if (D.BUG) Log.d(TAG, "tool:checkODICaptionsTooltip() putBoolean true"); 558 Prefs.putBoolean(mContext, 559 Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, true); 560 mHasSeenODICaptionsTooltip = true; 561 if (mODICaptionsIcon != null) { 562 mODICaptionsIcon 563 .postOnAnimation(getSinglePressFor(mODICaptionsIcon)); 564 } 565 }) 566 .start(); 567 } 568 } 569 hideCaptionsTooltip()570 private void hideCaptionsTooltip() { 571 if (mODICaptionsTooltipView != null && mODICaptionsTooltipView.getVisibility() == VISIBLE) { 572 mODICaptionsTooltipView.animate().cancel(); 573 mODICaptionsTooltipView.setAlpha(1.f); 574 mODICaptionsTooltipView.animate() 575 .alpha(0.f) 576 .setStartDelay(0) 577 .setDuration(DIALOG_HIDE_ANIMATION_DURATION) 578 .withEndAction(() -> mODICaptionsTooltipView.setVisibility(INVISIBLE)) 579 .start(); 580 } 581 } 582 tryToRemoveCaptionsTooltip()583 protected void tryToRemoveCaptionsTooltip() { 584 if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) { 585 ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container); 586 container.removeView(mODICaptionsTooltipView); 587 mODICaptionsTooltipView = null; 588 } 589 } 590 updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip)591 private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) { 592 if (mODICaptionsView != null) { 593 mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE); 594 } 595 596 if (!isServiceComponentEnabled) return; 597 598 updateCaptionsIcon(); 599 if (fromTooltip) showCaptionsTooltip(); 600 } 601 updateCaptionsIcon()602 private void updateCaptionsIcon() { 603 boolean captionsEnabled = mController.areCaptionsEnabled(); 604 if (mODICaptionsIcon.getCaptionsEnabled() != captionsEnabled) { 605 mHandler.post(mODICaptionsIcon.setCaptionsEnabled(captionsEnabled)); 606 } 607 608 boolean isOptedOut = mController.isCaptionStreamOptedOut(); 609 if (mODICaptionsIcon.getOptedOut() != isOptedOut) { 610 mHandler.post(() -> mODICaptionsIcon.setOptedOut(isOptedOut)); 611 } 612 } 613 onCaptionIconClicked()614 private void onCaptionIconClicked() { 615 boolean isEnabled = mController.areCaptionsEnabled(); 616 mController.setCaptionsEnabled(!isEnabled); 617 updateCaptionsIcon(); 618 } 619 incrementManualToggleCount()620 private void incrementManualToggleCount() { 621 ContentResolver cr = mContext.getContentResolver(); 622 int ringerCount = Settings.Secure.getInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, 0); 623 Settings.Secure.putInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, ringerCount + 1); 624 } 625 provideTouchFeedbackH(int newRingerMode)626 private void provideTouchFeedbackH(int newRingerMode) { 627 VibrationEffect effect = null; 628 switch (newRingerMode) { 629 case RINGER_MODE_NORMAL: 630 mController.scheduleTouchFeedback(); 631 break; 632 case RINGER_MODE_SILENT: 633 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); 634 break; 635 case RINGER_MODE_VIBRATE: 636 default: 637 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); 638 } 639 if (effect != null) { 640 mController.vibrate(effect); 641 } 642 } 643 maybeShowToastH(int newRingerMode)644 private void maybeShowToastH(int newRingerMode) { 645 int seenToastCount = Prefs.getInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, 0); 646 647 if (seenToastCount > VolumePrefs.SHOW_RINGER_TOAST_COUNT) { 648 return; 649 } 650 CharSequence toastText = null; 651 switch (newRingerMode) { 652 case RINGER_MODE_NORMAL: 653 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 654 if (ss != null) { 655 toastText = mContext.getString( 656 R.string.volume_dialog_ringer_guidance_ring, 657 Utils.formatPercentage(ss.level, ss.levelMax)); 658 } 659 break; 660 case RINGER_MODE_SILENT: 661 toastText = mContext.getString( 662 com.android.internal.R.string.volume_dialog_ringer_guidance_silent); 663 break; 664 case RINGER_MODE_VIBRATE: 665 default: 666 toastText = mContext.getString( 667 com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate); 668 } 669 670 Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT).show(); 671 seenToastCount++; 672 Prefs.putInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, seenToastCount); 673 } 674 show(int reason)675 public void show(int reason) { 676 mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); 677 } 678 dismiss(int reason)679 public void dismiss(int reason) { 680 mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); 681 } 682 showH(int reason)683 private void showH(int reason) { 684 if (D.BUG) Log.d(TAG, "showH r=" + Events.SHOW_REASONS[reason]); 685 mHandler.removeMessages(H.SHOW); 686 mHandler.removeMessages(H.DISMISS); 687 rescheduleTimeoutH(); 688 689 if (mConfigChanged) { 690 initDialog(); // resets mShowing to false 691 mConfigurableTexts.update(); 692 mConfigChanged = false; 693 } 694 695 initSettingsH(); 696 mShowing = true; 697 mIsAnimatingDismiss = false; 698 mDialog.show(); 699 Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); 700 mController.notifyVisible(true); 701 mController.getCaptionsComponentState(false); 702 checkODICaptionsTooltip(false); 703 } 704 rescheduleTimeoutH()705 protected void rescheduleTimeoutH() { 706 mHandler.removeMessages(H.DISMISS); 707 final int timeout = computeTimeoutH(); 708 mHandler.sendMessageDelayed(mHandler 709 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); 710 if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); 711 mController.userActivity(); 712 } 713 computeTimeoutH()714 private int computeTimeoutH() { 715 if (mHovering) { 716 return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_HOVERING_TIMEOUT_MILLIS, 717 AccessibilityManager.FLAG_CONTENT_CONTROLS); 718 } 719 if (mSafetyWarning != null) { 720 return mAccessibilityMgr.getRecommendedTimeoutMillis( 721 DIALOG_SAFETYWARNING_TIMEOUT_MILLIS, 722 AccessibilityManager.FLAG_CONTENT_TEXT 723 | AccessibilityManager.FLAG_CONTENT_CONTROLS); 724 } 725 if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) { 726 return mAccessibilityMgr.getRecommendedTimeoutMillis( 727 DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS, 728 AccessibilityManager.FLAG_CONTENT_TEXT 729 | AccessibilityManager.FLAG_CONTENT_CONTROLS); 730 } 731 return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_TIMEOUT_MILLIS, 732 AccessibilityManager.FLAG_CONTENT_CONTROLS); 733 } 734 dismissH(int reason)735 protected void dismissH(int reason) { 736 if (D.BUG) { 737 Log.d(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason] 738 + " from: " + Debug.getCaller()); 739 } 740 mHandler.removeMessages(H.DISMISS); 741 mHandler.removeMessages(H.SHOW); 742 if (mIsAnimatingDismiss) { 743 return; 744 } 745 mIsAnimatingDismiss = true; 746 mDialogView.animate().cancel(); 747 if (mShowing) { 748 mShowing = false; 749 // Only logs when the volume dialog visibility is changed. 750 Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason); 751 } 752 mDialogView.setTranslationX(0); 753 mDialogView.setAlpha(1); 754 ViewPropertyAnimator animator = mDialogView.animate() 755 .alpha(0) 756 .setDuration(DIALOG_HIDE_ANIMATION_DURATION) 757 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) 758 .withEndAction(() -> mHandler.postDelayed(() -> { 759 mDialog.dismiss(); 760 tryToRemoveCaptionsTooltip(); 761 mIsAnimatingDismiss = false; 762 }, 50)); 763 if (!isLandscape()) animator.translationX(mDialogView.getWidth() / 2.0f); 764 animator.start(); 765 checkODICaptionsTooltip(true); 766 mController.notifyVisible(false); 767 synchronized (mSafetyWarningLock) { 768 if (mSafetyWarning != null) { 769 if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); 770 mSafetyWarning.dismiss(); 771 } 772 } 773 } 774 showActiveStreamOnly()775 private boolean showActiveStreamOnly() { 776 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) 777 || mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION); 778 } 779 shouldBeVisibleH(VolumeRow row, VolumeRow activeRow)780 private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { 781 boolean isActive = row.stream == activeRow.stream; 782 783 if (isActive) { 784 return true; 785 } 786 787 if (!mShowActiveStreamOnly) { 788 if (row.stream == AudioSystem.STREAM_ACCESSIBILITY) { 789 return mShowA11yStream; 790 } 791 792 // if the active row is accessibility, then continue to display previous 793 // active row since accessibility is displayed under it 794 if (activeRow.stream == AudioSystem.STREAM_ACCESSIBILITY && 795 row.stream == mPrevActiveStream) { 796 return true; 797 } 798 799 if (row.defaultStream) { 800 return activeRow.stream == STREAM_RING 801 || activeRow.stream == STREAM_ALARM 802 || activeRow.stream == STREAM_VOICE_CALL 803 || activeRow.stream == STREAM_ACCESSIBILITY 804 || mDynamic.get(activeRow.stream); 805 } 806 } 807 808 return false; 809 } 810 updateRowsH(final VolumeRow activeRow)811 private void updateRowsH(final VolumeRow activeRow) { 812 if (D.BUG) Log.d(TAG, "updateRowsH"); 813 if (!mShowing) { 814 trimObsoleteH(); 815 } 816 // apply changes to all rows 817 for (final VolumeRow row : mRows) { 818 final boolean isActive = row == activeRow; 819 final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); 820 Util.setVisOrGone(row.view, shouldBeVisible); 821 if (row.view.isShown()) { 822 updateVolumeRowTintH(row, isActive); 823 } 824 } 825 } 826 updateRingerH()827 protected void updateRingerH() { 828 if (mRinger != null && mState != null) { 829 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 830 if (ss == null) { 831 return; 832 } 833 834 boolean isZenMuted = mState.zenMode == Global.ZEN_MODE_ALARMS 835 || mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS 836 || (mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS 837 && mState.disallowRinger); 838 enableRingerViewsH(!isZenMuted); 839 switch (mState.ringerModeInternal) { 840 case AudioManager.RINGER_MODE_VIBRATE: 841 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate); 842 addAccessibilityDescription(mRingerIcon, RINGER_MODE_VIBRATE, 843 mContext.getString(R.string.volume_ringer_hint_mute)); 844 mRingerIcon.setTag(Events.ICON_STATE_VIBRATE); 845 break; 846 case AudioManager.RINGER_MODE_SILENT: 847 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute); 848 mRingerIcon.setTag(Events.ICON_STATE_MUTE); 849 addAccessibilityDescription(mRingerIcon, RINGER_MODE_SILENT, 850 mContext.getString(R.string.volume_ringer_hint_unmute)); 851 break; 852 case AudioManager.RINGER_MODE_NORMAL: 853 default: 854 boolean muted = (mAutomute && ss.level == 0) || ss.muted; 855 if (!isZenMuted && muted) { 856 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute); 857 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 858 mContext.getString(R.string.volume_ringer_hint_unmute)); 859 mRingerIcon.setTag(Events.ICON_STATE_MUTE); 860 } else { 861 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer); 862 if (mController.hasVibrator()) { 863 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 864 mContext.getString(R.string.volume_ringer_hint_vibrate)); 865 } else { 866 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 867 mContext.getString(R.string.volume_ringer_hint_mute)); 868 } 869 mRingerIcon.setTag(Events.ICON_STATE_UNMUTE); 870 } 871 break; 872 } 873 } 874 } 875 addAccessibilityDescription(View view, int currState, String hintLabel)876 private void addAccessibilityDescription(View view, int currState, String hintLabel) { 877 int currStateResId; 878 switch (currState) { 879 case RINGER_MODE_SILENT: 880 currStateResId = R.string.volume_ringer_status_silent; 881 break; 882 case RINGER_MODE_VIBRATE: 883 currStateResId = R.string.volume_ringer_status_vibrate; 884 break; 885 case RINGER_MODE_NORMAL: 886 default: 887 currStateResId = R.string.volume_ringer_status_normal; 888 } 889 890 view.setContentDescription(mContext.getString(currStateResId)); 891 view.setAccessibilityDelegate(new AccessibilityDelegate() { 892 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 893 super.onInitializeAccessibilityNodeInfo(host, info); 894 info.addAction(new AccessibilityNodeInfo.AccessibilityAction( 895 AccessibilityNodeInfo.ACTION_CLICK, hintLabel)); 896 } 897 }); 898 } 899 900 /** 901 * Toggles enable state of views in a VolumeRow (not including seekbar or icon) 902 * Hides/shows zen icon 903 * @param enable whether to enable volume row views and hide dnd icon 904 */ enableVolumeRowViewsH(VolumeRow row, boolean enable)905 private void enableVolumeRowViewsH(VolumeRow row, boolean enable) { 906 boolean showDndIcon = !enable; 907 row.dndIcon.setVisibility(showDndIcon ? VISIBLE : GONE); 908 } 909 910 /** 911 * Toggles enable state of footer/ringer views 912 * Hides/shows zen icon 913 * @param enable whether to enable ringer views and hide dnd icon 914 */ enableRingerViewsH(boolean enable)915 private void enableRingerViewsH(boolean enable) { 916 if (mRingerIcon != null) { 917 mRingerIcon.setEnabled(enable); 918 } 919 if (mZenIcon != null) { 920 mZenIcon.setVisibility(enable ? GONE : VISIBLE); 921 } 922 } 923 trimObsoleteH()924 private void trimObsoleteH() { 925 if (D.BUG) Log.d(TAG, "trimObsoleteH"); 926 for (int i = mRows.size() - 1; i >= 0; i--) { 927 final VolumeRow row = mRows.get(i); 928 if (row.ss == null || !row.ss.dynamic) continue; 929 if (!mDynamic.get(row.stream)) { 930 mRows.remove(i); 931 mDialogRowsView.removeView(row.view); 932 mConfigurableTexts.remove(row.header); 933 } 934 } 935 } 936 onStateChangedH(State state)937 protected void onStateChangedH(State state) { 938 if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString()); 939 if (mState != null && state != null 940 && mState.ringerModeInternal != -1 941 && mState.ringerModeInternal != state.ringerModeInternal 942 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { 943 mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK)); 944 } 945 946 mState = state; 947 mDynamic.clear(); 948 // add any new dynamic rows 949 for (int i = 0; i < state.states.size(); i++) { 950 final int stream = state.states.keyAt(i); 951 final StreamState ss = state.states.valueAt(i); 952 if (!ss.dynamic) continue; 953 mDynamic.put(stream, true); 954 if (findRow(stream) == null) { 955 addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true, 956 false, true); 957 } 958 } 959 960 if (mActiveStream != state.activeStream) { 961 mPrevActiveStream = mActiveStream; 962 mActiveStream = state.activeStream; 963 VolumeRow activeRow = getActiveRow(); 964 updateRowsH(activeRow); 965 if (mShowing) rescheduleTimeoutH(); 966 } 967 for (VolumeRow row : mRows) { 968 updateVolumeRowH(row); 969 } 970 updateRingerH(); 971 mWindow.setTitle(composeWindowTitle()); 972 } 973 composeWindowTitle()974 CharSequence composeWindowTitle() { 975 return mContext.getString(R.string.volume_dialog_title, getStreamLabelH(getActiveRow().ss)); 976 } 977 updateVolumeRowH(VolumeRow row)978 private void updateVolumeRowH(VolumeRow row) { 979 if (D.BUG) Log.i(TAG, "updateVolumeRowH s=" + row.stream); 980 if (mState == null) return; 981 final StreamState ss = mState.states.get(row.stream); 982 if (ss == null) return; 983 row.ss = ss; 984 if (ss.level > 0) { 985 row.lastAudibleLevel = ss.level; 986 } 987 if (ss.level == row.requestedLevel) { 988 row.requestedLevel = -1; 989 } 990 final boolean isA11yStream = row.stream == STREAM_ACCESSIBILITY; 991 final boolean isRingStream = row.stream == AudioManager.STREAM_RING; 992 final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; 993 final boolean isAlarmStream = row.stream == STREAM_ALARM; 994 final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; 995 final boolean isRingVibrate = isRingStream 996 && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; 997 final boolean isRingSilent = isRingStream 998 && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; 999 final boolean isZenPriorityOnly = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 1000 final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; 1001 final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; 1002 final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream) 1003 : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream) 1004 : isZenPriorityOnly ? ((isAlarmStream && mState.disallowAlarms) || 1005 (isMusicStream && mState.disallowMedia) || 1006 (isRingStream && mState.disallowRinger) || 1007 (isSystemStream && mState.disallowSystem)) 1008 : false; 1009 1010 // update slider max 1011 final int max = ss.levelMax * 100; 1012 if (max != row.slider.getMax()) { 1013 row.slider.setMax(max); 1014 } 1015 // update slider min 1016 final int min = ss.levelMin * 100; 1017 if (min != row.slider.getMin()) { 1018 row.slider.setMin(min); 1019 } 1020 1021 // update header text 1022 Util.setText(row.header, getStreamLabelH(ss)); 1023 row.slider.setContentDescription(row.header.getText()); 1024 mConfigurableTexts.add(row.header, ss.name); 1025 1026 // update icon 1027 final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted; 1028 row.icon.setEnabled(iconEnabled); 1029 row.icon.setAlpha(iconEnabled ? 1 : 0.5f); 1030 final int iconRes = 1031 isRingVibrate ? R.drawable.ic_volume_ringer_vibrate 1032 : isRingSilent || zenMuted ? row.iconMuteRes 1033 : ss.routedToBluetooth 1034 ? isStreamMuted(ss) ? R.drawable.ic_volume_media_bt_mute 1035 : R.drawable.ic_volume_media_bt 1036 : isStreamMuted(ss) ? row.iconMuteRes : row.iconRes; 1037 row.icon.setImageResource(iconRes); 1038 row.iconState = 1039 iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE 1040 : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) 1041 ? Events.ICON_STATE_MUTE 1042 : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) 1043 ? Events.ICON_STATE_UNMUTE 1044 : Events.ICON_STATE_UNKNOWN; 1045 if (iconEnabled) { 1046 if (isRingStream) { 1047 if (isRingVibrate) { 1048 row.icon.setContentDescription(mContext.getString( 1049 R.string.volume_stream_content_description_unmute, 1050 getStreamLabelH(ss))); 1051 } else { 1052 if (mController.hasVibrator()) { 1053 row.icon.setContentDescription(mContext.getString( 1054 mShowA11yStream 1055 ? R.string.volume_stream_content_description_vibrate_a11y 1056 : R.string.volume_stream_content_description_vibrate, 1057 getStreamLabelH(ss))); 1058 } else { 1059 row.icon.setContentDescription(mContext.getString( 1060 mShowA11yStream 1061 ? R.string.volume_stream_content_description_mute_a11y 1062 : R.string.volume_stream_content_description_mute, 1063 getStreamLabelH(ss))); 1064 } 1065 } 1066 } else if (isA11yStream) { 1067 row.icon.setContentDescription(getStreamLabelH(ss)); 1068 } else { 1069 if (ss.muted || mAutomute && ss.level == 0) { 1070 row.icon.setContentDescription(mContext.getString( 1071 R.string.volume_stream_content_description_unmute, 1072 getStreamLabelH(ss))); 1073 } else { 1074 row.icon.setContentDescription(mContext.getString( 1075 mShowA11yStream 1076 ? R.string.volume_stream_content_description_mute_a11y 1077 : R.string.volume_stream_content_description_mute, 1078 getStreamLabelH(ss))); 1079 } 1080 } 1081 } else { 1082 row.icon.setContentDescription(getStreamLabelH(ss)); 1083 } 1084 1085 // ensure tracking is disabled if zenMuted 1086 if (zenMuted) { 1087 row.tracking = false; 1088 } 1089 enableVolumeRowViewsH(row, !zenMuted); 1090 1091 // update slider 1092 final boolean enableSlider = !zenMuted; 1093 final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0 1094 : row.ss.level; 1095 updateVolumeRowSliderH(row, enableSlider, vlevel); 1096 } 1097 isStreamMuted(final StreamState streamState)1098 private boolean isStreamMuted(final StreamState streamState) { 1099 return (mAutomute && streamState.level == 0) || streamState.muted; 1100 } 1101 updateVolumeRowTintH(VolumeRow row, boolean isActive)1102 private void updateVolumeRowTintH(VolumeRow row, boolean isActive) { 1103 if (isActive) { 1104 row.slider.requestFocus(); 1105 } 1106 boolean useActiveColoring = isActive && row.slider.isEnabled(); 1107 final ColorStateList tint = useActiveColoring 1108 ? Utils.getColorAccent(mContext) 1109 : Utils.getColorAttr(mContext, android.R.attr.colorForeground); 1110 final int alpha = useActiveColoring 1111 ? Color.alpha(tint.getDefaultColor()) 1112 : getAlphaAttr(android.R.attr.secondaryContentAlpha); 1113 if (tint == row.cachedTint) return; 1114 row.slider.setProgressTintList(tint); 1115 row.slider.setThumbTintList(tint); 1116 row.slider.setProgressBackgroundTintList(tint); 1117 row.slider.setAlpha(((float) alpha) / 255); 1118 row.icon.setImageTintList(tint); 1119 row.icon.setImageAlpha(alpha); 1120 row.cachedTint = tint; 1121 } 1122 updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel)1123 private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) { 1124 row.slider.setEnabled(enable); 1125 updateVolumeRowTintH(row, row.stream == mActiveStream); 1126 if (row.tracking) { 1127 return; // don't update if user is sliding 1128 } 1129 final int progress = row.slider.getProgress(); 1130 final int level = getImpliedLevel(row.slider, progress); 1131 final boolean rowVisible = row.view.getVisibility() == VISIBLE; 1132 final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) 1133 < USER_ATTEMPT_GRACE_PERIOD; 1134 mHandler.removeMessages(H.RECHECK, row); 1135 if (mShowing && rowVisible && inGracePeriod) { 1136 if (D.BUG) Log.d(TAG, "inGracePeriod"); 1137 mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row), 1138 row.userAttempt + USER_ATTEMPT_GRACE_PERIOD); 1139 return; // don't update if visible and in grace period 1140 } 1141 if (vlevel == level) { 1142 if (mShowing && rowVisible) { 1143 return; // don't clamp if visible 1144 } 1145 } 1146 final int newProgress = vlevel * 100; 1147 if (progress != newProgress) { 1148 if (mShowing && rowVisible) { 1149 // animate! 1150 if (row.anim != null && row.anim.isRunning() 1151 && row.animTargetProgress == newProgress) { 1152 return; // already animating to the target progress 1153 } 1154 // start/update animation 1155 if (row.anim == null) { 1156 row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); 1157 row.anim.setInterpolator(new DecelerateInterpolator()); 1158 } else { 1159 row.anim.cancel(); 1160 row.anim.setIntValues(progress, newProgress); 1161 } 1162 row.animTargetProgress = newProgress; 1163 row.anim.setDuration(UPDATE_ANIMATION_DURATION); 1164 row.anim.start(); 1165 } else { 1166 // update slider directly to clamped value 1167 if (row.anim != null) { 1168 row.anim.cancel(); 1169 } 1170 row.slider.setProgress(newProgress, true); 1171 } 1172 } 1173 } 1174 1175 private void recheckH(VolumeRow row) { 1176 if (row == null) { 1177 if (D.BUG) Log.d(TAG, "recheckH ALL"); 1178 trimObsoleteH(); 1179 for (VolumeRow r : mRows) { 1180 updateVolumeRowH(r); 1181 } 1182 } else { 1183 if (D.BUG) Log.d(TAG, "recheckH " + row.stream); 1184 updateVolumeRowH(row); 1185 } 1186 } 1187 1188 private void setStreamImportantH(int stream, boolean important) { 1189 for (VolumeRow row : mRows) { 1190 if (row.stream == stream) { 1191 row.important = important; 1192 return; 1193 } 1194 } 1195 } 1196 1197 private void showSafetyWarningH(int flags) { 1198 if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 1199 || mShowing) { 1200 synchronized (mSafetyWarningLock) { 1201 if (mSafetyWarning != null) { 1202 return; 1203 } 1204 mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { 1205 @Override 1206 protected void cleanUp() { 1207 synchronized (mSafetyWarningLock) { 1208 mSafetyWarning = null; 1209 } 1210 recheckH(null); 1211 } 1212 }; 1213 mSafetyWarning.show(); 1214 } 1215 recheckH(null); 1216 } 1217 rescheduleTimeoutH(); 1218 } 1219 1220 private String getStreamLabelH(StreamState ss) { 1221 if (ss == null) { 1222 return ""; 1223 } 1224 if (ss.remoteLabel != null) { 1225 return ss.remoteLabel; 1226 } 1227 try { 1228 return mContext.getResources().getString(ss.name); 1229 } catch (Resources.NotFoundException e) { 1230 Slog.e(TAG, "Can't find translation for stream " + ss); 1231 return ""; 1232 } 1233 } 1234 1235 private Runnable getSinglePressFor(ImageButton button) { 1236 return () -> { 1237 if (button != null) { 1238 button.setPressed(true); 1239 button.postOnAnimationDelayed(getSingleUnpressFor(button), 200); 1240 } 1241 }; 1242 } 1243 getSingleUnpressFor(ImageButton button)1244 private Runnable getSingleUnpressFor(ImageButton button) { 1245 return () -> { 1246 if (button != null) { 1247 button.setPressed(false); 1248 } 1249 }; 1250 } 1251 1252 private final VolumeDialogController.Callbacks mControllerCallbackH 1253 = new VolumeDialogController.Callbacks() { 1254 @Override 1255 public void onShowRequested(int reason) { 1256 showH(reason); 1257 } 1258 1259 @Override 1260 public void onDismissRequested(int reason) { 1261 dismissH(reason); 1262 } 1263 1264 @Override 1265 public void onScreenOff() { 1266 dismissH(Events.DISMISS_REASON_SCREEN_OFF); 1267 } 1268 1269 @Override 1270 public void onStateChanged(State state) { 1271 onStateChangedH(state); 1272 } 1273 1274 @Override 1275 public void onLayoutDirectionChanged(int layoutDirection) { 1276 mDialogView.setLayoutDirection(layoutDirection); 1277 } 1278 1279 @Override 1280 public void onConfigurationChanged() { 1281 mDialog.dismiss(); 1282 mConfigChanged = true; 1283 } 1284 1285 @Override 1286 public void onShowVibrateHint() { 1287 if (mSilentMode) { 1288 mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); 1289 } 1290 } 1291 1292 @Override 1293 public void onShowSilentHint() { 1294 if (mSilentMode) { 1295 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); 1296 } 1297 } 1298 1299 @Override 1300 public void onShowSafetyWarning(int flags) { 1301 showSafetyWarningH(flags); 1302 } 1303 1304 @Override 1305 public void onAccessibilityModeChanged(Boolean showA11yStream) { 1306 mShowA11yStream = showA11yStream == null ? false : showA11yStream; 1307 VolumeRow activeRow = getActiveRow(); 1308 if (!mShowA11yStream && STREAM_ACCESSIBILITY == activeRow.stream) { 1309 dismissH(Events.DISMISS_STREAM_GONE); 1310 } else { 1311 updateRowsH(activeRow); 1312 } 1313 1314 } 1315 1316 @Override 1317 public void onCaptionComponentStateChanged( 1318 Boolean isComponentEnabled, Boolean fromTooltip) { 1319 updateODICaptionsH(isComponentEnabled, fromTooltip); 1320 } 1321 }; 1322 1323 private final class H extends Handler { 1324 private static final int SHOW = 1; 1325 private static final int DISMISS = 2; 1326 private static final int RECHECK = 3; 1327 private static final int RECHECK_ALL = 4; 1328 private static final int SET_STREAM_IMPORTANT = 5; 1329 private static final int RESCHEDULE_TIMEOUT = 6; 1330 private static final int STATE_CHANGED = 7; 1331 1332 public H() { 1333 super(Looper.getMainLooper()); 1334 } 1335 1336 @Override 1337 public void handleMessage(Message msg) { 1338 switch (msg.what) { 1339 case SHOW: showH(msg.arg1); break; 1340 case DISMISS: dismissH(msg.arg1); break; 1341 case RECHECK: recheckH((VolumeRow) msg.obj); break; 1342 case RECHECK_ALL: recheckH(null); break; 1343 case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; 1344 case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; 1345 case STATE_CHANGED: onStateChangedH(mState); break; 1346 } 1347 } 1348 } 1349 1350 private final class CustomDialog extends Dialog implements DialogInterface { 1351 public CustomDialog(Context context) { 1352 super(context, R.style.qs_theme); 1353 } 1354 1355 @Override 1356 public boolean dispatchTouchEvent(MotionEvent ev) { 1357 rescheduleTimeoutH(); 1358 return super.dispatchTouchEvent(ev); 1359 } 1360 1361 @Override 1362 protected void onStart() { 1363 super.setCanceledOnTouchOutside(true); 1364 super.onStart(); 1365 } 1366 1367 @Override 1368 protected void onStop() { 1369 super.onStop(); 1370 mHandler.sendEmptyMessage(H.RECHECK_ALL); 1371 } 1372 1373 @Override 1374 public boolean onTouchEvent(MotionEvent event) { 1375 if (mShowing) { 1376 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1377 dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); 1378 return true; 1379 } 1380 } 1381 return false; 1382 } 1383 } 1384 1385 private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { 1386 private final VolumeRow mRow; 1387 1388 private VolumeSeekBarChangeListener(VolumeRow row) { 1389 mRow = row; 1390 } 1391 1392 @Override 1393 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1394 if (mRow.ss == null) return; 1395 if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) 1396 + " onProgressChanged " + progress + " fromUser=" + fromUser); 1397 if (!fromUser) return; 1398 if (mRow.ss.levelMin > 0) { 1399 final int minProgress = mRow.ss.levelMin * 100; 1400 if (progress < minProgress) { 1401 seekBar.setProgress(minProgress); 1402 progress = minProgress; 1403 } 1404 } 1405 final int userLevel = getImpliedLevel(seekBar, progress); 1406 if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { 1407 mRow.userAttempt = SystemClock.uptimeMillis(); 1408 if (mRow.requestedLevel != userLevel) { 1409 mController.setActiveStream(mRow.stream); 1410 mController.setStreamVolume(mRow.stream, userLevel); 1411 mRow.requestedLevel = userLevel; 1412 Events.writeEvent(Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, 1413 userLevel); 1414 } 1415 } 1416 } 1417 1418 @Override 1419 public void onStartTrackingTouch(SeekBar seekBar) { 1420 if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); 1421 mController.setActiveStream(mRow.stream); 1422 mRow.tracking = true; 1423 } 1424 1425 @Override 1426 public void onStopTrackingTouch(SeekBar seekBar) { 1427 if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); 1428 mRow.tracking = false; 1429 mRow.userAttempt = SystemClock.uptimeMillis(); 1430 final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); 1431 Events.writeEvent(Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); 1432 if (mRow.ss.level != userLevel) { 1433 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), 1434 USER_ATTEMPT_GRACE_PERIOD); 1435 } 1436 } 1437 } 1438 1439 private final class Accessibility extends AccessibilityDelegate { 1440 public void init() { 1441 mDialogView.setAccessibilityDelegate(this); 1442 } 1443 1444 @Override 1445 public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 1446 // Activities populate their title here. Follow that example. 1447 event.getText().add(composeWindowTitle()); 1448 return true; 1449 } 1450 1451 @Override 1452 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 1453 AccessibilityEvent event) { 1454 rescheduleTimeoutH(); 1455 return super.onRequestSendAccessibilityEvent(host, child, event); 1456 } 1457 } 1458 1459 private static class VolumeRow { 1460 private View view; 1461 private TextView header; 1462 private ImageButton icon; 1463 private SeekBar slider; 1464 private int stream; 1465 private StreamState ss; 1466 private long userAttempt; // last user-driven slider change 1467 private boolean tracking; // tracking slider touch 1468 private int requestedLevel = -1; // pending user-requested level via progress changed 1469 private int iconRes; 1470 private int iconMuteRes; 1471 private boolean important; 1472 private boolean defaultStream; 1473 private ColorStateList cachedTint; 1474 private int iconState; // from Events 1475 private ObjectAnimator anim; // slider progress animation for non-touch-related updates 1476 private int animTargetProgress; 1477 private int lastAudibleLevel = 1; 1478 private FrameLayout dndIcon; 1479 } 1480 } 1481