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.Gravity; 69 import android.view.MotionEvent; 70 import android.view.View; 71 import android.view.View.AccessibilityDelegate; 72 import android.view.ViewGroup; 73 import android.view.ViewPropertyAnimator; 74 import android.view.ViewStub; 75 import android.view.Window; 76 import android.view.WindowManager; 77 import android.view.accessibility.AccessibilityEvent; 78 import android.view.accessibility.AccessibilityManager; 79 import android.view.accessibility.AccessibilityNodeInfo; 80 import android.view.animation.DecelerateInterpolator; 81 import android.widget.FrameLayout; 82 import android.widget.ImageButton; 83 import android.widget.SeekBar; 84 import android.widget.SeekBar.OnSeekBarChangeListener; 85 import android.widget.TextView; 86 import android.widget.Toast; 87 88 import com.android.settingslib.Utils; 89 import com.android.systemui.Dependency; 90 import com.android.systemui.Prefs; 91 import com.android.systemui.R; 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 mHasSeenODICaptionsTooltip; 164 private ViewStub mODICaptionsTooltipViewStub; 165 private View mODICaptionsTooltipView = null; 166 VolumeDialogImpl(Context context)167 public VolumeDialogImpl(Context context) { 168 mContext = 169 new ContextThemeWrapper(context, R.style.qs_theme); 170 mController = Dependency.get(VolumeDialogController.class); 171 mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); 172 mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 173 mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class); 174 mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); 175 mShowActiveStreamOnly = showActiveStreamOnly(); 176 mHasSeenODICaptionsTooltip = 177 Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false); 178 } 179 180 @Override onUiModeChanged()181 public void onUiModeChanged() { 182 mContext.getTheme().applyStyle(mContext.getThemeResId(), true); 183 } 184 init(int windowType, Callback callback)185 public void init(int windowType, Callback callback) { 186 initDialog(); 187 188 mAccessibility.init(); 189 190 mController.addCallback(mControllerCallbackH, mHandler); 191 mController.getState(); 192 193 Dependency.get(ConfigurationController.class).addCallback(this); 194 } 195 196 @Override destroy()197 public void destroy() { 198 mController.removeCallback(mControllerCallbackH); 199 mHandler.removeCallbacksAndMessages(null); 200 Dependency.get(ConfigurationController.class).removeCallback(this); 201 } 202 initDialog()203 private void initDialog() { 204 mDialog = new CustomDialog(mContext); 205 206 mConfigurableTexts = new ConfigurableTexts(mContext); 207 mHovering = false; 208 mShowing = false; 209 mWindow = mDialog.getWindow(); 210 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 211 mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 212 mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND 213 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); 214 mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 215 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 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 = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 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(mContext, 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(mContext, Events.EVENT_SETTINGS_CLICK); 472 Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); 473 dismissH(DISMISS_REASON_SETTINGS_CLICKED); 474 Dependency.get(ActivityStarter.class).startActivity(intent, 475 true /* dismissShade */); 476 }); 477 } 478 } 479 initRingerH()480 public void initRingerH() { 481 if (mRingerIcon != null) { 482 mRingerIcon.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE); 483 mRingerIcon.setOnClickListener(v -> { 484 Prefs.putBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true); 485 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 486 if (ss == null) { 487 return; 488 } 489 // normal -> vibrate -> silent -> normal (skip vibrate if device doesn't have 490 // a vibrator. 491 int newRingerMode; 492 final boolean hasVibrator = mController.hasVibrator(); 493 if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { 494 if (hasVibrator) { 495 newRingerMode = AudioManager.RINGER_MODE_VIBRATE; 496 } else { 497 newRingerMode = AudioManager.RINGER_MODE_SILENT; 498 } 499 } else if (mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { 500 newRingerMode = AudioManager.RINGER_MODE_SILENT; 501 } else { 502 newRingerMode = AudioManager.RINGER_MODE_NORMAL; 503 if (ss.level == 0) { 504 mController.setStreamVolume(AudioManager.STREAM_RING, 1); 505 } 506 } 507 Events.writeEvent(mContext, Events.EVENT_RINGER_TOGGLE, newRingerMode); 508 incrementManualToggleCount(); 509 updateRingerH(); 510 provideTouchFeedbackH(newRingerMode); 511 mController.setRingerMode(newRingerMode, false); 512 maybeShowToastH(newRingerMode); 513 }); 514 } 515 updateRingerH(); 516 } 517 initODICaptionsH()518 private void initODICaptionsH() { 519 if (mODICaptionsIcon != null) { 520 mODICaptionsIcon.setOnConfirmedTapListener(() -> { 521 onCaptionIconClicked(); 522 Events.writeEvent(mContext, Events.EVENT_ODI_CAPTIONS_CLICK); 523 }, mHandler); 524 } 525 526 mController.getCaptionsComponentState(false); 527 } 528 checkODICaptionsTooltip(boolean fromDismiss)529 private void checkODICaptionsTooltip(boolean fromDismiss) { 530 if (!mHasSeenODICaptionsTooltip && !fromDismiss && mODICaptionsTooltipViewStub != null) { 531 mController.getCaptionsComponentState(true); 532 } else { 533 if (mHasSeenODICaptionsTooltip && fromDismiss && mODICaptionsTooltipView != null) { 534 hideCaptionsTooltip(); 535 } 536 } 537 } 538 showCaptionsTooltip()539 protected void showCaptionsTooltip() { 540 if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipViewStub != null) { 541 mODICaptionsTooltipView = mODICaptionsTooltipViewStub.inflate(); 542 mODICaptionsTooltipView.findViewById(R.id.dismiss).setOnClickListener(v -> { 543 hideCaptionsTooltip(); 544 Events.writeEvent(mContext, Events.EVENT_ODI_CAPTIONS_TOOLTIP_CLICK); 545 }); 546 mODICaptionsTooltipViewStub = null; 547 rescheduleTimeoutH(); 548 } 549 550 if (mODICaptionsTooltipView != null) { 551 mODICaptionsTooltipView.setAlpha(0.f); 552 mODICaptionsTooltipView.animate() 553 .alpha(1.f) 554 .setStartDelay(DIALOG_SHOW_ANIMATION_DURATION) 555 .withEndAction(() -> { 556 if (D.BUG) Log.d(TAG, "tool:checkODICaptionsTooltip() putBoolean true"); 557 Prefs.putBoolean(mContext, 558 Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, true); 559 mHasSeenODICaptionsTooltip = true; 560 if (mODICaptionsIcon != null) { 561 mODICaptionsIcon 562 .postOnAnimation(getSinglePressFor(mODICaptionsIcon)); 563 } 564 }) 565 .start(); 566 } 567 } 568 hideCaptionsTooltip()569 private void hideCaptionsTooltip() { 570 if (mODICaptionsTooltipView != null && mODICaptionsTooltipView.getVisibility() == VISIBLE) { 571 mODICaptionsTooltipView.animate().cancel(); 572 mODICaptionsTooltipView.setAlpha(1.f); 573 mODICaptionsTooltipView.animate() 574 .alpha(0.f) 575 .setStartDelay(0) 576 .setDuration(DIALOG_HIDE_ANIMATION_DURATION) 577 .withEndAction(() -> mODICaptionsTooltipView.setVisibility(INVISIBLE)) 578 .start(); 579 } 580 } 581 tryToRemoveCaptionsTooltip()582 protected void tryToRemoveCaptionsTooltip() { 583 if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) { 584 ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container); 585 container.removeView(mODICaptionsTooltipView); 586 mODICaptionsTooltipView = null; 587 } 588 } 589 updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip)590 private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) { 591 if (mODICaptionsView != null) { 592 mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE); 593 } 594 595 if (!isServiceComponentEnabled) return; 596 597 updateCaptionsIcon(); 598 if (fromTooltip) showCaptionsTooltip(); 599 } 600 updateCaptionsIcon()601 private void updateCaptionsIcon() { 602 boolean captionsEnabled = mController.areCaptionsEnabled(); 603 if (mODICaptionsIcon.getCaptionsEnabled() != captionsEnabled) { 604 mHandler.post(mODICaptionsIcon.setCaptionsEnabled(captionsEnabled)); 605 } 606 607 boolean isOptedOut = mController.isCaptionStreamOptedOut(); 608 if (mODICaptionsIcon.getOptedOut() != isOptedOut) { 609 mHandler.post(() -> mODICaptionsIcon.setOptedOut(isOptedOut)); 610 } 611 } 612 onCaptionIconClicked()613 private void onCaptionIconClicked() { 614 boolean isEnabled = mController.areCaptionsEnabled(); 615 mController.setCaptionsEnabled(!isEnabled); 616 updateCaptionsIcon(); 617 } 618 incrementManualToggleCount()619 private void incrementManualToggleCount() { 620 ContentResolver cr = mContext.getContentResolver(); 621 int ringerCount = Settings.Secure.getInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, 0); 622 Settings.Secure.putInt(cr, Settings.Secure.MANUAL_RINGER_TOGGLE_COUNT, ringerCount + 1); 623 } 624 provideTouchFeedbackH(int newRingerMode)625 private void provideTouchFeedbackH(int newRingerMode) { 626 VibrationEffect effect = null; 627 switch (newRingerMode) { 628 case RINGER_MODE_NORMAL: 629 mController.scheduleTouchFeedback(); 630 break; 631 case RINGER_MODE_SILENT: 632 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); 633 break; 634 case RINGER_MODE_VIBRATE: 635 default: 636 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); 637 } 638 if (effect != null) { 639 mController.vibrate(effect); 640 } 641 } 642 maybeShowToastH(int newRingerMode)643 private void maybeShowToastH(int newRingerMode) { 644 int seenToastCount = Prefs.getInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, 0); 645 646 if (seenToastCount > VolumePrefs.SHOW_RINGER_TOAST_COUNT) { 647 return; 648 } 649 CharSequence toastText = null; 650 switch (newRingerMode) { 651 case RINGER_MODE_NORMAL: 652 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 653 if (ss != null) { 654 toastText = mContext.getString( 655 R.string.volume_dialog_ringer_guidance_ring, 656 Utils.formatPercentage(ss.level, ss.levelMax)); 657 } 658 break; 659 case RINGER_MODE_SILENT: 660 toastText = mContext.getString( 661 com.android.internal.R.string.volume_dialog_ringer_guidance_silent); 662 break; 663 case RINGER_MODE_VIBRATE: 664 default: 665 toastText = mContext.getString( 666 com.android.internal.R.string.volume_dialog_ringer_guidance_vibrate); 667 } 668 669 Toast.makeText(mContext, toastText, Toast.LENGTH_SHORT).show(); 670 seenToastCount++; 671 Prefs.putInt(mContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, seenToastCount); 672 } 673 show(int reason)674 public void show(int reason) { 675 mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); 676 } 677 dismiss(int reason)678 public void dismiss(int reason) { 679 mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); 680 } 681 showH(int reason)682 private void showH(int reason) { 683 if (D.BUG) Log.d(TAG, "showH r=" + Events.SHOW_REASONS[reason]); 684 mHandler.removeMessages(H.SHOW); 685 mHandler.removeMessages(H.DISMISS); 686 rescheduleTimeoutH(); 687 688 if (mConfigChanged) { 689 initDialog(); // resets mShowing to false 690 mConfigurableTexts.update(); 691 mConfigChanged = false; 692 } 693 694 initSettingsH(); 695 mShowing = true; 696 mDialog.show(); 697 Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); 698 mController.notifyVisible(true); 699 mController.getCaptionsComponentState(false); 700 checkODICaptionsTooltip(false); 701 } 702 rescheduleTimeoutH()703 protected void rescheduleTimeoutH() { 704 mHandler.removeMessages(H.DISMISS); 705 final int timeout = computeTimeoutH(); 706 mHandler.sendMessageDelayed(mHandler 707 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); 708 if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); 709 mController.userActivity(); 710 } 711 computeTimeoutH()712 private int computeTimeoutH() { 713 if (mHovering) { 714 return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_HOVERING_TIMEOUT_MILLIS, 715 AccessibilityManager.FLAG_CONTENT_CONTROLS); 716 } 717 if (mSafetyWarning != null) { 718 return mAccessibilityMgr.getRecommendedTimeoutMillis( 719 DIALOG_SAFETYWARNING_TIMEOUT_MILLIS, 720 AccessibilityManager.FLAG_CONTENT_TEXT 721 | AccessibilityManager.FLAG_CONTENT_CONTROLS); 722 } 723 if (!mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) { 724 return mAccessibilityMgr.getRecommendedTimeoutMillis( 725 DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS, 726 AccessibilityManager.FLAG_CONTENT_TEXT 727 | AccessibilityManager.FLAG_CONTENT_CONTROLS); 728 } 729 return mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_TIMEOUT_MILLIS, 730 AccessibilityManager.FLAG_CONTENT_CONTROLS); 731 } 732 dismissH(int reason)733 protected void dismissH(int reason) { 734 if (D.BUG) { 735 Log.d(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason] 736 + " from: " + Debug.getCaller()); 737 } 738 mHandler.removeMessages(H.DISMISS); 739 mHandler.removeMessages(H.SHOW); 740 mDialogView.animate().cancel(); 741 if (mShowing) { 742 mShowing = false; 743 // Only logs when the volume dialog visibility is changed. 744 Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); 745 } 746 mDialogView.setTranslationX(0); 747 mDialogView.setAlpha(1); 748 ViewPropertyAnimator animator = mDialogView.animate() 749 .alpha(0) 750 .setDuration(DIALOG_HIDE_ANIMATION_DURATION) 751 .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) 752 .withEndAction(() -> mHandler.postDelayed(() -> { 753 mDialog.dismiss(); 754 tryToRemoveCaptionsTooltip(); 755 }, 50)); 756 if (!isLandscape()) animator.translationX(mDialogView.getWidth() / 2.0f); 757 animator.start(); 758 checkODICaptionsTooltip(true); 759 mController.notifyVisible(false); 760 synchronized (mSafetyWarningLock) { 761 if (mSafetyWarning != null) { 762 if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); 763 mSafetyWarning.dismiss(); 764 } 765 } 766 } 767 showActiveStreamOnly()768 private boolean showActiveStreamOnly() { 769 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) 770 || mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION); 771 } 772 shouldBeVisibleH(VolumeRow row, VolumeRow activeRow)773 private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { 774 boolean isActive = row.stream == activeRow.stream; 775 776 if (isActive) { 777 return true; 778 } 779 780 if (!mShowActiveStreamOnly) { 781 if (row.stream == AudioSystem.STREAM_ACCESSIBILITY) { 782 return mShowA11yStream; 783 } 784 785 // if the active row is accessibility, then continue to display previous 786 // active row since accessibility is displayed under it 787 if (activeRow.stream == AudioSystem.STREAM_ACCESSIBILITY && 788 row.stream == mPrevActiveStream) { 789 return true; 790 } 791 792 if (row.defaultStream) { 793 return activeRow.stream == STREAM_RING 794 || activeRow.stream == STREAM_ALARM 795 || activeRow.stream == STREAM_VOICE_CALL 796 || activeRow.stream == STREAM_ACCESSIBILITY 797 || mDynamic.get(activeRow.stream); 798 } 799 } 800 801 return false; 802 } 803 updateRowsH(final VolumeRow activeRow)804 private void updateRowsH(final VolumeRow activeRow) { 805 if (D.BUG) Log.d(TAG, "updateRowsH"); 806 if (!mShowing) { 807 trimObsoleteH(); 808 } 809 // apply changes to all rows 810 for (final VolumeRow row : mRows) { 811 final boolean isActive = row == activeRow; 812 final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); 813 Util.setVisOrGone(row.view, shouldBeVisible); 814 if (row.view.isShown()) { 815 updateVolumeRowTintH(row, isActive); 816 } 817 } 818 } 819 updateRingerH()820 protected void updateRingerH() { 821 if (mState != null) { 822 final StreamState ss = mState.states.get(AudioManager.STREAM_RING); 823 if (ss == null) { 824 return; 825 } 826 827 boolean isZenMuted = mState.zenMode == Global.ZEN_MODE_ALARMS 828 || mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS 829 || (mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS 830 && mState.disallowRinger); 831 enableRingerViewsH(!isZenMuted); 832 switch (mState.ringerModeInternal) { 833 case AudioManager.RINGER_MODE_VIBRATE: 834 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate); 835 addAccessibilityDescription(mRingerIcon, RINGER_MODE_VIBRATE, 836 mContext.getString(R.string.volume_ringer_hint_mute)); 837 mRingerIcon.setTag(Events.ICON_STATE_VIBRATE); 838 break; 839 case AudioManager.RINGER_MODE_SILENT: 840 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute); 841 mRingerIcon.setTag(Events.ICON_STATE_MUTE); 842 addAccessibilityDescription(mRingerIcon, RINGER_MODE_SILENT, 843 mContext.getString(R.string.volume_ringer_hint_unmute)); 844 break; 845 case AudioManager.RINGER_MODE_NORMAL: 846 default: 847 boolean muted = (mAutomute && ss.level == 0) || ss.muted; 848 if (!isZenMuted && muted) { 849 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute); 850 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 851 mContext.getString(R.string.volume_ringer_hint_unmute)); 852 mRingerIcon.setTag(Events.ICON_STATE_MUTE); 853 } else { 854 mRingerIcon.setImageResource(R.drawable.ic_volume_ringer); 855 if (mController.hasVibrator()) { 856 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 857 mContext.getString(R.string.volume_ringer_hint_vibrate)); 858 } else { 859 addAccessibilityDescription(mRingerIcon, RINGER_MODE_NORMAL, 860 mContext.getString(R.string.volume_ringer_hint_mute)); 861 } 862 mRingerIcon.setTag(Events.ICON_STATE_UNMUTE); 863 } 864 break; 865 } 866 } 867 } 868 addAccessibilityDescription(View view, int currState, String hintLabel)869 private void addAccessibilityDescription(View view, int currState, String hintLabel) { 870 int currStateResId; 871 switch (currState) { 872 case RINGER_MODE_SILENT: 873 currStateResId = R.string.volume_ringer_status_silent; 874 break; 875 case RINGER_MODE_VIBRATE: 876 currStateResId = R.string.volume_ringer_status_vibrate; 877 break; 878 case RINGER_MODE_NORMAL: 879 default: 880 currStateResId = R.string.volume_ringer_status_normal; 881 } 882 883 view.setContentDescription(mContext.getString(currStateResId)); 884 view.setAccessibilityDelegate(new AccessibilityDelegate() { 885 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 886 super.onInitializeAccessibilityNodeInfo(host, info); 887 info.addAction(new AccessibilityNodeInfo.AccessibilityAction( 888 AccessibilityNodeInfo.ACTION_CLICK, hintLabel)); 889 } 890 }); 891 } 892 893 /** 894 * Toggles enable state of views in a VolumeRow (not including seekbar or icon) 895 * Hides/shows zen icon 896 * @param enable whether to enable volume row views and hide dnd icon 897 */ enableVolumeRowViewsH(VolumeRow row, boolean enable)898 private void enableVolumeRowViewsH(VolumeRow row, boolean enable) { 899 boolean showDndIcon = !enable; 900 row.dndIcon.setVisibility(showDndIcon ? VISIBLE : GONE); 901 } 902 903 /** 904 * Toggles enable state of footer/ringer views 905 * Hides/shows zen icon 906 * @param enable whether to enable ringer views and hide dnd icon 907 */ enableRingerViewsH(boolean enable)908 private void enableRingerViewsH(boolean enable) { 909 if (mRingerIcon != null) { 910 mRingerIcon.setEnabled(enable); 911 } 912 if (mZenIcon != null) { 913 mZenIcon.setVisibility(enable ? GONE : VISIBLE); 914 } 915 } 916 trimObsoleteH()917 private void trimObsoleteH() { 918 if (D.BUG) Log.d(TAG, "trimObsoleteH"); 919 for (int i = mRows.size() - 1; i >= 0; i--) { 920 final VolumeRow row = mRows.get(i); 921 if (row.ss == null || !row.ss.dynamic) continue; 922 if (!mDynamic.get(row.stream)) { 923 mRows.remove(i); 924 mDialogRowsView.removeView(row.view); 925 } 926 } 927 } 928 onStateChangedH(State state)929 protected void onStateChangedH(State state) { 930 if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString()); 931 if (mState != null && state != null 932 && mState.ringerModeInternal != state.ringerModeInternal 933 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { 934 mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK)); 935 } 936 937 mState = state; 938 mDynamic.clear(); 939 // add any new dynamic rows 940 for (int i = 0; i < state.states.size(); i++) { 941 final int stream = state.states.keyAt(i); 942 final StreamState ss = state.states.valueAt(i); 943 if (!ss.dynamic) continue; 944 mDynamic.put(stream, true); 945 if (findRow(stream) == null) { 946 addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true, 947 false, true); 948 } 949 } 950 951 if (mActiveStream != state.activeStream) { 952 mPrevActiveStream = mActiveStream; 953 mActiveStream = state.activeStream; 954 VolumeRow activeRow = getActiveRow(); 955 updateRowsH(activeRow); 956 if (mShowing) rescheduleTimeoutH(); 957 } 958 for (VolumeRow row : mRows) { 959 updateVolumeRowH(row); 960 } 961 updateRingerH(); 962 mWindow.setTitle(composeWindowTitle()); 963 } 964 composeWindowTitle()965 CharSequence composeWindowTitle() { 966 return mContext.getString(R.string.volume_dialog_title, getStreamLabelH(getActiveRow().ss)); 967 } 968 updateVolumeRowH(VolumeRow row)969 private void updateVolumeRowH(VolumeRow row) { 970 if (D.BUG) Log.i(TAG, "updateVolumeRowH s=" + row.stream); 971 if (mState == null) return; 972 final StreamState ss = mState.states.get(row.stream); 973 if (ss == null) return; 974 row.ss = ss; 975 if (ss.level > 0) { 976 row.lastAudibleLevel = ss.level; 977 } 978 if (ss.level == row.requestedLevel) { 979 row.requestedLevel = -1; 980 } 981 final boolean isA11yStream = row.stream == STREAM_ACCESSIBILITY; 982 final boolean isRingStream = row.stream == AudioManager.STREAM_RING; 983 final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; 984 final boolean isAlarmStream = row.stream == STREAM_ALARM; 985 final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; 986 final boolean isRingVibrate = isRingStream 987 && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; 988 final boolean isRingSilent = isRingStream 989 && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; 990 final boolean isZenPriorityOnly = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 991 final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; 992 final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; 993 final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream) 994 : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream) 995 : isZenPriorityOnly ? ((isAlarmStream && mState.disallowAlarms) || 996 (isMusicStream && mState.disallowMedia) || 997 (isRingStream && mState.disallowRinger) || 998 (isSystemStream && mState.disallowSystem)) 999 : false; 1000 1001 // update slider max 1002 final int max = ss.levelMax * 100; 1003 if (max != row.slider.getMax()) { 1004 row.slider.setMax(max); 1005 } 1006 // update slider min 1007 final int min = ss.levelMin * 100; 1008 if (min != row.slider.getMin()) { 1009 row.slider.setMin(min); 1010 } 1011 1012 // update header text 1013 Util.setText(row.header, getStreamLabelH(ss)); 1014 row.slider.setContentDescription(row.header.getText()); 1015 mConfigurableTexts.add(row.header, ss.name); 1016 1017 // update icon 1018 final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted; 1019 row.icon.setEnabled(iconEnabled); 1020 row.icon.setAlpha(iconEnabled ? 1 : 0.5f); 1021 final int iconRes = 1022 isRingVibrate ? R.drawable.ic_volume_ringer_vibrate 1023 : isRingSilent || zenMuted ? row.iconMuteRes 1024 : ss.routedToBluetooth ? 1025 (ss.muted ? R.drawable.ic_volume_media_bt_mute 1026 : R.drawable.ic_volume_media_bt) 1027 : mAutomute && ss.level == 0 ? row.iconMuteRes 1028 : (ss.muted ? row.iconMuteRes : row.iconRes); 1029 row.icon.setImageResource(iconRes); 1030 row.iconState = 1031 iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE 1032 : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) 1033 ? Events.ICON_STATE_MUTE 1034 : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) 1035 ? Events.ICON_STATE_UNMUTE 1036 : Events.ICON_STATE_UNKNOWN; 1037 if (iconEnabled) { 1038 if (isRingStream) { 1039 if (isRingVibrate) { 1040 row.icon.setContentDescription(mContext.getString( 1041 R.string.volume_stream_content_description_unmute, 1042 getStreamLabelH(ss))); 1043 } else { 1044 if (mController.hasVibrator()) { 1045 row.icon.setContentDescription(mContext.getString( 1046 mShowA11yStream 1047 ? R.string.volume_stream_content_description_vibrate_a11y 1048 : R.string.volume_stream_content_description_vibrate, 1049 getStreamLabelH(ss))); 1050 } else { 1051 row.icon.setContentDescription(mContext.getString( 1052 mShowA11yStream 1053 ? R.string.volume_stream_content_description_mute_a11y 1054 : R.string.volume_stream_content_description_mute, 1055 getStreamLabelH(ss))); 1056 } 1057 } 1058 } else if (isA11yStream) { 1059 row.icon.setContentDescription(getStreamLabelH(ss)); 1060 } else { 1061 if (ss.muted || mAutomute && ss.level == 0) { 1062 row.icon.setContentDescription(mContext.getString( 1063 R.string.volume_stream_content_description_unmute, 1064 getStreamLabelH(ss))); 1065 } else { 1066 row.icon.setContentDescription(mContext.getString( 1067 mShowA11yStream 1068 ? R.string.volume_stream_content_description_mute_a11y 1069 : R.string.volume_stream_content_description_mute, 1070 getStreamLabelH(ss))); 1071 } 1072 } 1073 } else { 1074 row.icon.setContentDescription(getStreamLabelH(ss)); 1075 } 1076 1077 // ensure tracking is disabled if zenMuted 1078 if (zenMuted) { 1079 row.tracking = false; 1080 } 1081 enableVolumeRowViewsH(row, !zenMuted); 1082 1083 // update slider 1084 final boolean enableSlider = !zenMuted; 1085 final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0 1086 : row.ss.level; 1087 updateVolumeRowSliderH(row, enableSlider, vlevel); 1088 } 1089 updateVolumeRowTintH(VolumeRow row, boolean isActive)1090 private void updateVolumeRowTintH(VolumeRow row, boolean isActive) { 1091 if (isActive) { 1092 row.slider.requestFocus(); 1093 } 1094 boolean useActiveColoring = isActive && row.slider.isEnabled(); 1095 final ColorStateList tint = useActiveColoring 1096 ? Utils.getColorAccent(mContext) 1097 : Utils.getColorAttr(mContext, android.R.attr.colorForeground); 1098 final int alpha = useActiveColoring 1099 ? Color.alpha(tint.getDefaultColor()) 1100 : getAlphaAttr(android.R.attr.secondaryContentAlpha); 1101 if (tint == row.cachedTint) return; 1102 row.slider.setProgressTintList(tint); 1103 row.slider.setThumbTintList(tint); 1104 row.slider.setProgressBackgroundTintList(tint); 1105 row.slider.setAlpha(((float) alpha) / 255); 1106 row.icon.setImageTintList(tint); 1107 row.icon.setImageAlpha(alpha); 1108 row.cachedTint = tint; 1109 } 1110 updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel)1111 private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) { 1112 row.slider.setEnabled(enable); 1113 updateVolumeRowTintH(row, row.stream == mActiveStream); 1114 if (row.tracking) { 1115 return; // don't update if user is sliding 1116 } 1117 final int progress = row.slider.getProgress(); 1118 final int level = getImpliedLevel(row.slider, progress); 1119 final boolean rowVisible = row.view.getVisibility() == VISIBLE; 1120 final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) 1121 < USER_ATTEMPT_GRACE_PERIOD; 1122 mHandler.removeMessages(H.RECHECK, row); 1123 if (mShowing && rowVisible && inGracePeriod) { 1124 if (D.BUG) Log.d(TAG, "inGracePeriod"); 1125 mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row), 1126 row.userAttempt + USER_ATTEMPT_GRACE_PERIOD); 1127 return; // don't update if visible and in grace period 1128 } 1129 if (vlevel == level) { 1130 if (mShowing && rowVisible) { 1131 return; // don't clamp if visible 1132 } 1133 } 1134 final int newProgress = vlevel * 100; 1135 if (progress != newProgress) { 1136 if (mShowing && rowVisible) { 1137 // animate! 1138 if (row.anim != null && row.anim.isRunning() 1139 && row.animTargetProgress == newProgress) { 1140 return; // already animating to the target progress 1141 } 1142 // start/update animation 1143 if (row.anim == null) { 1144 row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); 1145 row.anim.setInterpolator(new DecelerateInterpolator()); 1146 } else { 1147 row.anim.cancel(); 1148 row.anim.setIntValues(progress, newProgress); 1149 } 1150 row.animTargetProgress = newProgress; 1151 row.anim.setDuration(UPDATE_ANIMATION_DURATION); 1152 row.anim.start(); 1153 } else { 1154 // update slider directly to clamped value 1155 if (row.anim != null) { 1156 row.anim.cancel(); 1157 } 1158 row.slider.setProgress(newProgress, true); 1159 } 1160 } 1161 } 1162 1163 private void recheckH(VolumeRow row) { 1164 if (row == null) { 1165 if (D.BUG) Log.d(TAG, "recheckH ALL"); 1166 trimObsoleteH(); 1167 for (VolumeRow r : mRows) { 1168 updateVolumeRowH(r); 1169 } 1170 } else { 1171 if (D.BUG) Log.d(TAG, "recheckH " + row.stream); 1172 updateVolumeRowH(row); 1173 } 1174 } 1175 1176 private void setStreamImportantH(int stream, boolean important) { 1177 for (VolumeRow row : mRows) { 1178 if (row.stream == stream) { 1179 row.important = important; 1180 return; 1181 } 1182 } 1183 } 1184 1185 private void showSafetyWarningH(int flags) { 1186 if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 1187 || mShowing) { 1188 synchronized (mSafetyWarningLock) { 1189 if (mSafetyWarning != null) { 1190 return; 1191 } 1192 mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { 1193 @Override 1194 protected void cleanUp() { 1195 synchronized (mSafetyWarningLock) { 1196 mSafetyWarning = null; 1197 } 1198 recheckH(null); 1199 } 1200 }; 1201 mSafetyWarning.show(); 1202 } 1203 recheckH(null); 1204 } 1205 rescheduleTimeoutH(); 1206 } 1207 1208 private String getStreamLabelH(StreamState ss) { 1209 if (ss == null) { 1210 return ""; 1211 } 1212 if (ss.remoteLabel != null) { 1213 return ss.remoteLabel; 1214 } 1215 try { 1216 return mContext.getResources().getString(ss.name); 1217 } catch (Resources.NotFoundException e) { 1218 Slog.e(TAG, "Can't find translation for stream " + ss); 1219 return ""; 1220 } 1221 } 1222 1223 private Runnable getSinglePressFor(ImageButton button) { 1224 return () -> { 1225 if (button != null) { 1226 button.setPressed(true); 1227 button.postOnAnimationDelayed(getSingleUnpressFor(button), 200); 1228 } 1229 }; 1230 } 1231 getSingleUnpressFor(ImageButton button)1232 private Runnable getSingleUnpressFor(ImageButton button) { 1233 return () -> { 1234 if (button != null) { 1235 button.setPressed(false); 1236 } 1237 }; 1238 } 1239 1240 private final VolumeDialogController.Callbacks mControllerCallbackH 1241 = new VolumeDialogController.Callbacks() { 1242 @Override 1243 public void onShowRequested(int reason) { 1244 showH(reason); 1245 } 1246 1247 @Override 1248 public void onDismissRequested(int reason) { 1249 dismissH(reason); 1250 } 1251 1252 @Override 1253 public void onScreenOff() { 1254 dismissH(Events.DISMISS_REASON_SCREEN_OFF); 1255 } 1256 1257 @Override 1258 public void onStateChanged(State state) { 1259 onStateChangedH(state); 1260 } 1261 1262 @Override 1263 public void onLayoutDirectionChanged(int layoutDirection) { 1264 mDialogView.setLayoutDirection(layoutDirection); 1265 } 1266 1267 @Override 1268 public void onConfigurationChanged() { 1269 mDialog.dismiss(); 1270 mConfigChanged = true; 1271 } 1272 1273 @Override 1274 public void onShowVibrateHint() { 1275 if (mSilentMode) { 1276 mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); 1277 } 1278 } 1279 1280 @Override 1281 public void onShowSilentHint() { 1282 if (mSilentMode) { 1283 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); 1284 } 1285 } 1286 1287 @Override 1288 public void onShowSafetyWarning(int flags) { 1289 showSafetyWarningH(flags); 1290 } 1291 1292 @Override 1293 public void onAccessibilityModeChanged(Boolean showA11yStream) { 1294 mShowA11yStream = showA11yStream == null ? false : showA11yStream; 1295 VolumeRow activeRow = getActiveRow(); 1296 if (!mShowA11yStream && STREAM_ACCESSIBILITY == activeRow.stream) { 1297 dismissH(Events.DISMISS_STREAM_GONE); 1298 } else { 1299 updateRowsH(activeRow); 1300 } 1301 1302 } 1303 1304 @Override 1305 public void onCaptionComponentStateChanged( 1306 Boolean isComponentEnabled, Boolean fromTooltip) { 1307 updateODICaptionsH(isComponentEnabled, fromTooltip); 1308 } 1309 }; 1310 1311 private final class H extends Handler { 1312 private static final int SHOW = 1; 1313 private static final int DISMISS = 2; 1314 private static final int RECHECK = 3; 1315 private static final int RECHECK_ALL = 4; 1316 private static final int SET_STREAM_IMPORTANT = 5; 1317 private static final int RESCHEDULE_TIMEOUT = 6; 1318 private static final int STATE_CHANGED = 7; 1319 1320 public H() { 1321 super(Looper.getMainLooper()); 1322 } 1323 1324 @Override 1325 public void handleMessage(Message msg) { 1326 switch (msg.what) { 1327 case SHOW: showH(msg.arg1); break; 1328 case DISMISS: dismissH(msg.arg1); break; 1329 case RECHECK: recheckH((VolumeRow) msg.obj); break; 1330 case RECHECK_ALL: recheckH(null); break; 1331 case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; 1332 case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; 1333 case STATE_CHANGED: onStateChangedH(mState); break; 1334 } 1335 } 1336 } 1337 1338 private final class CustomDialog extends Dialog implements DialogInterface { 1339 public CustomDialog(Context context) { 1340 super(context, R.style.qs_theme); 1341 } 1342 1343 @Override 1344 public boolean dispatchTouchEvent(MotionEvent ev) { 1345 rescheduleTimeoutH(); 1346 return super.dispatchTouchEvent(ev); 1347 } 1348 1349 @Override 1350 protected void onStart() { 1351 super.setCanceledOnTouchOutside(true); 1352 super.onStart(); 1353 } 1354 1355 @Override 1356 protected void onStop() { 1357 super.onStop(); 1358 mHandler.sendEmptyMessage(H.RECHECK_ALL); 1359 } 1360 1361 @Override 1362 public boolean onTouchEvent(MotionEvent event) { 1363 if (mShowing) { 1364 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1365 dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); 1366 return true; 1367 } 1368 } 1369 return false; 1370 } 1371 } 1372 1373 private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { 1374 private final VolumeRow mRow; 1375 1376 private VolumeSeekBarChangeListener(VolumeRow row) { 1377 mRow = row; 1378 } 1379 1380 @Override 1381 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1382 if (mRow.ss == null) return; 1383 if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) 1384 + " onProgressChanged " + progress + " fromUser=" + fromUser); 1385 if (!fromUser) return; 1386 if (mRow.ss.levelMin > 0) { 1387 final int minProgress = mRow.ss.levelMin * 100; 1388 if (progress < minProgress) { 1389 seekBar.setProgress(minProgress); 1390 progress = minProgress; 1391 } 1392 } 1393 final int userLevel = getImpliedLevel(seekBar, progress); 1394 if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { 1395 mRow.userAttempt = SystemClock.uptimeMillis(); 1396 if (mRow.requestedLevel != userLevel) { 1397 mController.setActiveStream(mRow.stream); 1398 mController.setStreamVolume(mRow.stream, userLevel); 1399 mRow.requestedLevel = userLevel; 1400 Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, 1401 userLevel); 1402 } 1403 } 1404 } 1405 1406 @Override 1407 public void onStartTrackingTouch(SeekBar seekBar) { 1408 if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); 1409 mController.setActiveStream(mRow.stream); 1410 mRow.tracking = true; 1411 } 1412 1413 @Override 1414 public void onStopTrackingTouch(SeekBar seekBar) { 1415 if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); 1416 mRow.tracking = false; 1417 mRow.userAttempt = SystemClock.uptimeMillis(); 1418 final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); 1419 Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); 1420 if (mRow.ss.level != userLevel) { 1421 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), 1422 USER_ATTEMPT_GRACE_PERIOD); 1423 } 1424 } 1425 } 1426 1427 private final class Accessibility extends AccessibilityDelegate { 1428 public void init() { 1429 mDialogView.setAccessibilityDelegate(this); 1430 } 1431 1432 @Override 1433 public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 1434 // Activities populate their title here. Follow that example. 1435 event.getText().add(composeWindowTitle()); 1436 return true; 1437 } 1438 1439 @Override 1440 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 1441 AccessibilityEvent event) { 1442 rescheduleTimeoutH(); 1443 return super.onRequestSendAccessibilityEvent(host, child, event); 1444 } 1445 } 1446 1447 private static class VolumeRow { 1448 private View view; 1449 private TextView header; 1450 private ImageButton icon; 1451 private SeekBar slider; 1452 private int stream; 1453 private StreamState ss; 1454 private long userAttempt; // last user-driven slider change 1455 private boolean tracking; // tracking slider touch 1456 private int requestedLevel = -1; // pending user-requested level via progress changed 1457 private int iconRes; 1458 private int iconMuteRes; 1459 private boolean important; 1460 private boolean defaultStream; 1461 private ColorStateList cachedTint; 1462 private int iconState; // from Events 1463 private ObjectAnimator anim; // slider progress animation for non-touch-related updates 1464 private int animTargetProgress; 1465 private int lastAudibleLevel = 1; 1466 private FrameLayout dndIcon; 1467 } 1468 } 1469