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.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; 20 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC; 21 22 import android.accessibilityservice.AccessibilityServiceInfo; 23 import android.animation.LayoutTransition; 24 import android.animation.ObjectAnimator; 25 import android.animation.ValueAnimator; 26 import android.annotation.SuppressLint; 27 import android.app.Dialog; 28 import android.app.KeyguardManager; 29 import android.content.Context; 30 import android.content.res.ColorStateList; 31 import android.content.res.Resources; 32 import android.graphics.Color; 33 import android.graphics.PixelFormat; 34 import android.graphics.Rect; 35 import android.graphics.drawable.AnimatedVectorDrawable; 36 import android.graphics.drawable.ColorDrawable; 37 import android.graphics.drawable.Drawable; 38 import android.media.AudioManager; 39 import android.media.AudioSystem; 40 import android.os.Debug; 41 import android.os.Handler; 42 import android.os.Looper; 43 import android.os.Message; 44 import android.os.SystemClock; 45 import android.provider.Settings.Global; 46 import android.util.DisplayMetrics; 47 import android.util.Log; 48 import android.util.SparseBooleanArray; 49 import android.view.Gravity; 50 import android.view.MotionEvent; 51 import android.view.View; 52 import android.view.View.AccessibilityDelegate; 53 import android.view.View.OnAttachStateChangeListener; 54 import android.view.View.OnClickListener; 55 import android.view.View.OnLayoutChangeListener; 56 import android.view.View.OnTouchListener; 57 import android.view.ViewGroup; 58 import android.view.ViewGroup.MarginLayoutParams; 59 import android.view.Window; 60 import android.view.WindowManager; 61 import android.view.accessibility.AccessibilityEvent; 62 import android.view.accessibility.AccessibilityManager; 63 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; 64 import android.view.animation.DecelerateInterpolator; 65 import android.widget.ImageButton; 66 import android.widget.LinearLayout; 67 import android.widget.SeekBar; 68 import android.widget.SeekBar.OnSeekBarChangeListener; 69 import android.widget.TextView; 70 71 import com.android.systemui.R; 72 import com.android.systemui.statusbar.policy.ZenModeController; 73 import com.android.systemui.volume.VolumeDialogController.State; 74 import com.android.systemui.volume.VolumeDialogController.StreamState; 75 76 import java.io.PrintWriter; 77 import java.util.ArrayList; 78 import java.util.List; 79 80 /** 81 * Visual presentation of the volume dialog. 82 * 83 * A client of VolumeDialogController and its state model. 84 * 85 * Methods ending in "H" must be called on the (ui) handler. 86 */ 87 public class VolumeDialog { 88 private static final String TAG = Util.logTag(VolumeDialog.class); 89 90 private static final long USER_ATTEMPT_GRACE_PERIOD = 1000; 91 private static final int WAIT_FOR_RIPPLE = 200; 92 private static final int UPDATE_ANIMATION_DURATION = 80; 93 94 private final Context mContext; 95 private final H mHandler = new H(); 96 private final VolumeDialogController mController; 97 98 private final CustomDialog mDialog; 99 private final ViewGroup mDialogView; 100 private final ViewGroup mDialogContentView; 101 private final ImageButton mExpandButton; 102 private final View mSettingsButton; 103 private final List<VolumeRow> mRows = new ArrayList<VolumeRow>(); 104 private final SpTexts mSpTexts; 105 private final SparseBooleanArray mDynamic = new SparseBooleanArray(); 106 private final KeyguardManager mKeyguard; 107 private final AudioManager mAudioManager; 108 private final int mExpandButtonAnimationDuration; 109 private final ZenFooter mZenFooter; 110 private final LayoutTransition mLayoutTransition; 111 private final Object mSafetyWarningLock = new Object(); 112 private final Accessibility mAccessibility = new Accessibility(); 113 private final ColorStateList mActiveSliderTint; 114 private final ColorStateList mInactiveSliderTint; 115 private final VolumeDialogMotion mMotion; 116 117 private boolean mShowing; 118 private boolean mExpanded; 119 private int mActiveStream; 120 private boolean mShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS; 121 private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE; 122 private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE; 123 private State mState; 124 private int mExpandButtonRes; 125 private boolean mExpandButtonAnimationRunning; 126 private SafetyWarningDialog mSafetyWarning; 127 private Callback mCallback; 128 private boolean mPendingStateChanged; 129 private boolean mPendingRecheckAll; 130 private long mCollapseTime; 131 VolumeDialog(Context context, int windowType, VolumeDialogController controller, ZenModeController zenModeController, Callback callback)132 public VolumeDialog(Context context, int windowType, VolumeDialogController controller, 133 ZenModeController zenModeController, Callback callback) { 134 mContext = context; 135 mController = controller; 136 mCallback = callback; 137 mSpTexts = new SpTexts(mContext); 138 mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 139 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 140 141 mDialog = new CustomDialog(mContext); 142 143 final Window window = mDialog.getWindow(); 144 window.requestFeature(Window.FEATURE_NO_TITLE); 145 window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 146 window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 147 window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 148 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 149 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 150 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 151 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 152 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); 153 mDialog.setCanceledOnTouchOutside(true); 154 final Resources res = mContext.getResources(); 155 final WindowManager.LayoutParams lp = window.getAttributes(); 156 lp.type = windowType; 157 lp.format = PixelFormat.TRANSLUCENT; 158 lp.setTitle(VolumeDialog.class.getSimpleName()); 159 lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 160 lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top); 161 lp.gravity = Gravity.TOP; 162 lp.windowAnimations = -1; 163 window.setAttributes(lp); 164 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); 165 166 mActiveSliderTint = loadColorStateList(R.color.system_accent_color); 167 mInactiveSliderTint = loadColorStateList(R.color.volume_slider_inactive); 168 mDialog.setContentView(R.layout.volume_dialog); 169 mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog); 170 mDialogContentView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog_content); 171 mExpandButton = (ImageButton) mDialogView.findViewById(R.id.volume_expand_button); 172 mExpandButton.setOnClickListener(mClickExpand); 173 updateWindowWidthH(); 174 updateExpandButtonH(); 175 mLayoutTransition = new LayoutTransition(); 176 mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2); 177 mDialogContentView.setLayoutTransition(mLayoutTransition); 178 mMotion = new VolumeDialogMotion(mDialog, mDialogView, mDialogContentView, mExpandButton, 179 new VolumeDialogMotion.Callback() { 180 @Override 181 public void onAnimatingChanged(boolean animating) { 182 if (animating) return; 183 if (mPendingStateChanged) { 184 mHandler.sendEmptyMessage(H.STATE_CHANGED); 185 mPendingStateChanged = false; 186 } 187 if (mPendingRecheckAll) { 188 mHandler.sendEmptyMessage(H.RECHECK_ALL); 189 mPendingRecheckAll = false; 190 } 191 } 192 }); 193 194 addRow(AudioManager.STREAM_RING, 195 R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true); 196 addRow(AudioManager.STREAM_MUSIC, 197 R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true); 198 addRow(AudioManager.STREAM_ALARM, 199 R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false); 200 addRow(AudioManager.STREAM_VOICE_CALL, 201 R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false); 202 addRow(AudioManager.STREAM_BLUETOOTH_SCO, 203 R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false); 204 addRow(AudioManager.STREAM_SYSTEM, 205 R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false); 206 207 mSettingsButton = mDialog.findViewById(R.id.volume_settings_button); 208 mSettingsButton.setOnClickListener(mClickSettings); 209 mExpandButtonAnimationDuration = res.getInteger(R.integer.volume_expand_animation_duration); 210 mZenFooter = (ZenFooter) mDialog.findViewById(R.id.volume_zen_footer); 211 mZenFooter.init(zenModeController); 212 213 mAccessibility.init(); 214 215 controller.addCallback(mControllerCallbackH, mHandler); 216 controller.getState(); 217 } 218 loadColorStateList(int colorResId)219 private ColorStateList loadColorStateList(int colorResId) { 220 return ColorStateList.valueOf(mContext.getColor(colorResId)); 221 } 222 updateWindowWidthH()223 private void updateWindowWidthH() { 224 final ViewGroup.LayoutParams lp = mDialogView.getLayoutParams(); 225 final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); 226 if (D.BUG) Log.d(TAG, "updateWindowWidth dm.w=" + dm.widthPixels); 227 int w = dm.widthPixels; 228 final int max = mContext.getResources() 229 .getDimensionPixelSize(R.dimen.standard_notification_panel_width); 230 if (w > max) { 231 w = max; 232 } 233 w -= mContext.getResources().getDimensionPixelSize(R.dimen.notification_side_padding) * 2; 234 lp.width = w; 235 mDialogView.setLayoutParams(lp); 236 } 237 setStreamImportant(int stream, boolean important)238 public void setStreamImportant(int stream, boolean important) { 239 mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget(); 240 } 241 setShowHeaders(boolean showHeaders)242 public void setShowHeaders(boolean showHeaders) { 243 if (showHeaders == mShowHeaders) return; 244 mShowHeaders = showHeaders; 245 mHandler.sendEmptyMessage(H.RECHECK_ALL); 246 } 247 setAutomute(boolean automute)248 public void setAutomute(boolean automute) { 249 if (mAutomute == automute) return; 250 mAutomute = automute; 251 mHandler.sendEmptyMessage(H.RECHECK_ALL); 252 } 253 setSilentMode(boolean silentMode)254 public void setSilentMode(boolean silentMode) { 255 if (mSilentMode == silentMode) return; 256 mSilentMode = silentMode; 257 mHandler.sendEmptyMessage(H.RECHECK_ALL); 258 } 259 addRow(int stream, int iconRes, int iconMuteRes, boolean important)260 private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) { 261 final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important); 262 if (!mRows.isEmpty()) { 263 final View v = new View(mContext); 264 v.setId(android.R.id.background); 265 final int h = mContext.getResources() 266 .getDimensionPixelSize(R.dimen.volume_slider_interspacing); 267 final LinearLayout.LayoutParams lp = 268 new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h); 269 mDialogContentView.addView(v, mDialogContentView.getChildCount() - 1, lp); 270 row.space = v; 271 } 272 row.settingsButton.addOnLayoutChangeListener(new OnLayoutChangeListener() { 273 @Override 274 public void onLayoutChange(View v, int left, int top, int right, int bottom, 275 int oldLeft, int oldTop, int oldRight, int oldBottom) { 276 final boolean moved = oldLeft != left || oldTop != top; 277 if (D.BUG) Log.d(TAG, "onLayoutChange moved=" + moved 278 + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString() 279 + " new=" + new Rect(left,top,right,bottom).toShortString()); 280 if (moved) { 281 for (int i = 0; i < mDialogContentView.getChildCount(); i++) { 282 final View c = mDialogContentView.getChildAt(i); 283 if (!c.isShown()) continue; 284 if (c == row.view) { 285 repositionExpandAnim(row); 286 } 287 return; 288 } 289 } 290 } 291 }); 292 // add new row just before the footer 293 mDialogContentView.addView(row.view, mDialogContentView.getChildCount() - 1); 294 mRows.add(row); 295 } 296 isAttached()297 private boolean isAttached() { 298 return mDialogContentView != null && mDialogContentView.isAttachedToWindow(); 299 } 300 getActiveRow()301 private VolumeRow getActiveRow() { 302 for (VolumeRow row : mRows) { 303 if (row.stream == mActiveStream) { 304 return row; 305 } 306 } 307 return mRows.get(0); 308 } 309 findRow(int stream)310 private VolumeRow findRow(int stream) { 311 for (VolumeRow row : mRows) { 312 if (row.stream == stream) return row; 313 } 314 return null; 315 } 316 repositionExpandAnim(VolumeRow row)317 private void repositionExpandAnim(VolumeRow row) { 318 final int[] loc = new int[2]; 319 row.settingsButton.getLocationInWindow(loc); 320 final MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams(); 321 final int x = loc[0] - mlp.leftMargin; 322 final int y = loc[1] - mlp.topMargin; 323 if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y); 324 mExpandButton.setTranslationX(x); 325 mExpandButton.setTranslationY(y); 326 mExpandButton.setTag((Integer) y); 327 } 328 dump(PrintWriter writer)329 public void dump(PrintWriter writer) { 330 writer.println(VolumeDialog.class.getSimpleName() + " state:"); 331 writer.print(" mShowing: "); writer.println(mShowing); 332 writer.print(" mExpanded: "); writer.println(mExpanded); 333 writer.print(" mExpandButtonAnimationRunning: "); 334 writer.println(mExpandButtonAnimationRunning); 335 writer.print(" mActiveStream: "); writer.println(mActiveStream); 336 writer.print(" mDynamic: "); writer.println(mDynamic); 337 writer.print(" mShowHeaders: "); writer.println(mShowHeaders); 338 writer.print(" mAutomute: "); writer.println(mAutomute); 339 writer.print(" mSilentMode: "); writer.println(mSilentMode); 340 writer.print(" mCollapseTime: "); writer.println(mCollapseTime); 341 writer.print(" mAccessibility.mFeedbackEnabled: "); 342 writer.println(mAccessibility.mFeedbackEnabled); 343 } 344 getImpliedLevel(SeekBar seekBar, int progress)345 private static int getImpliedLevel(SeekBar seekBar, int progress) { 346 final int m = seekBar.getMax(); 347 final int n = m / 100 - 1; 348 final int level = progress == 0 ? 0 349 : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n)); 350 return level; 351 } 352 353 @SuppressLint("InflateParams") initRow(final int stream, int iconRes, int iconMuteRes, boolean important)354 private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) { 355 final VolumeRow row = new VolumeRow(); 356 row.stream = stream; 357 row.iconRes = iconRes; 358 row.iconMuteRes = iconMuteRes; 359 row.important = important; 360 row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null); 361 row.view.setTag(row); 362 row.header = (TextView) row.view.findViewById(R.id.volume_row_header); 363 mSpTexts.add(row.header); 364 row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider); 365 row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); 366 367 // forward events above the slider into the slider 368 row.view.setOnTouchListener(new OnTouchListener() { 369 private final Rect mSliderHitRect = new Rect(); 370 private boolean mDragging; 371 372 @SuppressLint("ClickableViewAccessibility") 373 @Override 374 public boolean onTouch(View v, MotionEvent event) { 375 row.slider.getHitRect(mSliderHitRect); 376 if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN 377 && event.getY() < mSliderHitRect.top) { 378 mDragging = true; 379 } 380 if (mDragging) { 381 event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top); 382 row.slider.dispatchTouchEvent(event); 383 if (event.getActionMasked() == MotionEvent.ACTION_UP 384 || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { 385 mDragging = false; 386 } 387 return true; 388 } 389 return false; 390 } 391 }); 392 row.icon = (ImageButton) row.view.findViewById(R.id.volume_row_icon); 393 row.icon.setImageResource(iconRes); 394 row.icon.setOnClickListener(new OnClickListener() { 395 @Override 396 public void onClick(View v) { 397 Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState); 398 mController.setActiveStream(row.stream); 399 if (row.stream == AudioManager.STREAM_RING) { 400 final boolean hasVibrator = mController.hasVibrator(); 401 if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { 402 if (hasVibrator) { 403 mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); 404 } else { 405 final boolean wasZero = row.ss.level == 0; 406 mController.setStreamVolume(stream, wasZero ? row.lastAudibleLevel : 0); 407 } 408 } else { 409 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); 410 if (row.ss.level == 0) { 411 mController.setStreamVolume(stream, 1); 412 } 413 } 414 } else { 415 final boolean vmute = row.ss.level == 0; 416 mController.setStreamVolume(stream, vmute ? row.lastAudibleLevel : 0); 417 } 418 row.userAttempt = 0; // reset the grace period, slider should update immediately 419 } 420 }); 421 row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button); 422 row.settingsButton.setOnClickListener(mClickSettings); 423 return row; 424 } 425 destroy()426 public void destroy() { 427 mController.removeCallback(mControllerCallbackH); 428 } 429 show(int reason)430 public void show(int reason) { 431 mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); 432 } 433 dismiss(int reason)434 public void dismiss(int reason) { 435 mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); 436 } 437 showH(int reason)438 private void showH(int reason) { 439 if (D.BUG) Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]); 440 mHandler.removeMessages(H.SHOW); 441 mHandler.removeMessages(H.DISMISS); 442 rescheduleTimeoutH(); 443 if (mShowing) return; 444 mShowing = true; 445 mMotion.startShow(); 446 Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); 447 mController.notifyVisible(true); 448 } 449 rescheduleTimeoutH()450 protected void rescheduleTimeoutH() { 451 mHandler.removeMessages(H.DISMISS); 452 final int timeout = computeTimeoutH(); 453 mHandler.sendMessageDelayed(mHandler 454 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); 455 if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); 456 mController.userActivity(); 457 } 458 computeTimeoutH()459 private int computeTimeoutH() { 460 if (mAccessibility.mFeedbackEnabled) return 20000; 461 if (mSafetyWarning != null) return 5000; 462 if (mExpanded || mExpandButtonAnimationRunning) return 5000; 463 if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500; 464 return 3000; 465 } 466 dismissH(int reason)467 protected void dismissH(int reason) { 468 if (mMotion.isAnimating()) { 469 return; 470 } 471 mHandler.removeMessages(H.DISMISS); 472 mHandler.removeMessages(H.SHOW); 473 if (!mShowing) return; 474 mShowing = false; 475 mMotion.startDismiss(new Runnable() { 476 @Override 477 public void run() { 478 setExpandedH(false); 479 } 480 }); 481 Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); 482 mController.notifyVisible(false); 483 synchronized (mSafetyWarningLock) { 484 if (mSafetyWarning != null) { 485 if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); 486 mSafetyWarning.dismiss(); 487 } 488 } 489 } 490 updateDialogBottomMarginH()491 private void updateDialogBottomMarginH() { 492 final long diff = System.currentTimeMillis() - mCollapseTime; 493 final boolean collapsing = mCollapseTime != 0 && diff < getConservativeCollapseDuration(); 494 final ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams(); 495 final int bottomMargin = collapsing ? mDialogContentView.getHeight() : 496 mContext.getResources().getDimensionPixelSize(R.dimen.volume_dialog_margin_bottom); 497 if (bottomMargin != mlp.bottomMargin) { 498 if (D.BUG) Log.d(TAG, "bottomMargin " + mlp.bottomMargin + " -> " + bottomMargin); 499 mlp.bottomMargin = bottomMargin; 500 mDialogView.setLayoutParams(mlp); 501 } 502 } 503 504 private long getConservativeCollapseDuration() { 505 return mExpandButtonAnimationDuration * 3; 506 } 507 508 private void prepareForCollapse() { 509 mHandler.removeMessages(H.UPDATE_BOTTOM_MARGIN); 510 mCollapseTime = System.currentTimeMillis(); 511 updateDialogBottomMarginH(); 512 mHandler.sendEmptyMessageDelayed(H.UPDATE_BOTTOM_MARGIN, getConservativeCollapseDuration()); 513 } 514 515 private void setExpandedH(boolean expanded) { 516 if (mExpanded == expanded) return; 517 mExpanded = expanded; 518 mExpandButtonAnimationRunning = isAttached(); 519 if (D.BUG) Log.d(TAG, "setExpandedH " + expanded); 520 if (!mExpanded && mExpandButtonAnimationRunning) { 521 prepareForCollapse(); 522 } 523 updateRowsH(); 524 if (mExpandButtonAnimationRunning) { 525 final Drawable d = mExpandButton.getDrawable(); 526 if (d instanceof AnimatedVectorDrawable) { 527 // workaround to reset drawable 528 final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState() 529 .newDrawable(); 530 mExpandButton.setImageDrawable(avd); 531 avd.start(); 532 mHandler.postDelayed(new Runnable() { 533 @Override 534 public void run() { 535 mExpandButtonAnimationRunning = false; 536 updateExpandButtonH(); 537 rescheduleTimeoutH(); 538 } 539 }, mExpandButtonAnimationDuration); 540 } 541 } 542 rescheduleTimeoutH(); 543 } 544 545 private void updateExpandButtonH() { 546 if (D.BUG) Log.d(TAG, "updateExpandButtonH"); 547 mExpandButton.setClickable(!mExpandButtonAnimationRunning); 548 if (mExpandButtonAnimationRunning && isAttached()) return; 549 final int res = mExpanded ? R.drawable.ic_volume_collapse_animation 550 : R.drawable.ic_volume_expand_animation; 551 if (res == mExpandButtonRes) return; 552 mExpandButtonRes = res; 553 mExpandButton.setImageResource(res); 554 mExpandButton.setContentDescription(mContext.getString(mExpanded ? 555 R.string.accessibility_volume_collapse : R.string.accessibility_volume_expand)); 556 } 557 558 private boolean isVisibleH(VolumeRow row, boolean isActive) { 559 return mExpanded && row.view.getVisibility() == View.VISIBLE 560 || (mExpanded && (row.important || isActive)) 561 || !mExpanded && isActive; 562 } 563 564 private void updateRowsH() { 565 if (D.BUG) Log.d(TAG, "updateRowsH"); 566 final VolumeRow activeRow = getActiveRow(); 567 updateFooterH(); 568 updateExpandButtonH(); 569 if (!mShowing) { 570 trimObsoleteH(); 571 } 572 // apply changes to all rows 573 for (VolumeRow row : mRows) { 574 final boolean isActive = row == activeRow; 575 final boolean visible = isVisibleH(row, isActive); 576 Util.setVisOrGone(row.view, visible); 577 Util.setVisOrGone(row.space, visible && mExpanded); 578 final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0; 579 if (expandButtonRes != row.cachedExpandButtonRes) { 580 row.cachedExpandButtonRes = expandButtonRes; 581 if (expandButtonRes == 0) { 582 row.settingsButton.setImageDrawable(null); 583 } else { 584 row.settingsButton.setImageResource(expandButtonRes); 585 } 586 } 587 Util.setVisOrInvis(row.settingsButton, false); 588 updateVolumeRowHeaderVisibleH(row); 589 row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f); 590 updateVolumeRowSliderTintH(row, isActive); 591 } 592 } 593 594 private void trimObsoleteH() { 595 if (D.BUG) Log.d(TAG, "trimObsoleteH"); 596 for (int i = mRows.size() -1; i >= 0; i--) { 597 final VolumeRow row = mRows.get(i); 598 if (row.ss == null || !row.ss.dynamic) continue; 599 if (!mDynamic.get(row.stream)) { 600 mRows.remove(i); 601 mDialogContentView.removeView(row.view); 602 mDialogContentView.removeView(row.space); 603 } 604 } 605 } 606 onStateChangedH(State state)607 private void onStateChangedH(State state) { 608 final boolean animating = mMotion.isAnimating(); 609 if (D.BUG) Log.d(TAG, "onStateChangedH animating=" + animating); 610 mState = state; 611 if (animating) { 612 mPendingStateChanged = true; 613 return; 614 } 615 mDynamic.clear(); 616 // add any new dynamic rows 617 for (int i = 0; i < state.states.size(); i++) { 618 final int stream = state.states.keyAt(i); 619 final StreamState ss = state.states.valueAt(i); 620 if (!ss.dynamic) continue; 621 mDynamic.put(stream, true); 622 if (findRow(stream) == null) { 623 addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true); 624 } 625 } 626 627 if (mActiveStream != state.activeStream) { 628 mActiveStream = state.activeStream; 629 updateRowsH(); 630 rescheduleTimeoutH(); 631 } 632 for (VolumeRow row : mRows) { 633 updateVolumeRowH(row); 634 } 635 updateFooterH(); 636 } 637 updateFooterH()638 private void updateFooterH() { 639 if (D.BUG) Log.d(TAG, "updateFooterH"); 640 final boolean wasVisible = mZenFooter.getVisibility() == View.VISIBLE; 641 final boolean visible = mState.zenMode != Global.ZEN_MODE_OFF 642 && mAudioManager.isStreamAffectedByRingerMode(mActiveStream); 643 if (wasVisible != visible && !visible) { 644 prepareForCollapse(); 645 } 646 Util.setVisOrGone(mZenFooter, visible); 647 mZenFooter.update(); 648 } 649 updateVolumeRowH(VolumeRow row)650 private void updateVolumeRowH(VolumeRow row) { 651 if (D.BUG) Log.d(TAG, "updateVolumeRowH s=" + row.stream); 652 if (mState == null) return; 653 final StreamState ss = mState.states.get(row.stream); 654 if (ss == null) return; 655 row.ss = ss; 656 if (ss.level > 0) { 657 row.lastAudibleLevel = ss.level; 658 } 659 if (ss.level == row.requestedLevel) { 660 row.requestedLevel = -1; 661 } 662 final boolean isRingStream = row.stream == AudioManager.STREAM_RING; 663 final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; 664 final boolean isAlarmStream = row.stream == AudioManager.STREAM_ALARM; 665 final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; 666 final boolean isRingVibrate = isRingStream 667 && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; 668 final boolean isRingSilent = isRingStream 669 && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; 670 final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; 671 final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; 672 final boolean isZenPriority = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 673 final boolean isRingZenNone = (isRingStream || isSystemStream) && isZenNone; 674 final boolean isRingLimited = isRingStream && isZenPriority; 675 final boolean zenMuted = isZenAlarms ? (isRingStream || isSystemStream) 676 : isZenNone ? (isRingStream || isSystemStream || isAlarmStream || isMusicStream) 677 : false; 678 679 // update slider max 680 final int max = ss.levelMax * 100; 681 if (max != row.slider.getMax()) { 682 row.slider.setMax(max); 683 } 684 685 // update header visible 686 updateVolumeRowHeaderVisibleH(row); 687 688 // update header text 689 String text = ss.name; 690 if (mShowHeaders) { 691 if (isRingZenNone) { 692 text = mContext.getString(R.string.volume_stream_muted_dnd, ss.name); 693 } else if (isRingVibrate && isRingLimited) { 694 text = mContext.getString(R.string.volume_stream_vibrate_dnd, ss.name); 695 } else if (isRingVibrate) { 696 text = mContext.getString(R.string.volume_stream_vibrate, ss.name); 697 } else if (ss.muted || mAutomute && ss.level == 0) { 698 text = mContext.getString(R.string.volume_stream_muted, ss.name); 699 } else if (isRingLimited) { 700 text = mContext.getString(R.string.volume_stream_limited_dnd, ss.name); 701 } 702 } 703 Util.setText(row.header, text); 704 705 // update icon 706 final boolean iconEnabled = (mAutomute || ss.muteSupported) && !zenMuted; 707 row.icon.setEnabled(iconEnabled); 708 row.icon.setAlpha(iconEnabled ? 1 : 0.5f); 709 final int iconRes = 710 isRingVibrate ? R.drawable.ic_volume_ringer_vibrate 711 : isRingSilent || zenMuted ? row.cachedIconRes 712 : ss.routedToBluetooth ? 713 (ss.muted ? R.drawable.ic_volume_media_bt_mute 714 : R.drawable.ic_volume_media_bt) 715 : mAutomute && ss.level == 0 ? row.iconMuteRes 716 : (ss.muted ? row.iconMuteRes : row.iconRes); 717 if (iconRes != row.cachedIconRes) { 718 if (row.cachedIconRes != 0 && isRingVibrate) { 719 mController.vibrate(); 720 } 721 row.cachedIconRes = iconRes; 722 row.icon.setImageResource(iconRes); 723 } 724 row.iconState = 725 iconRes == R.drawable.ic_volume_ringer_vibrate ? Events.ICON_STATE_VIBRATE 726 : (iconRes == R.drawable.ic_volume_media_bt_mute || iconRes == row.iconMuteRes) 727 ? Events.ICON_STATE_MUTE 728 : (iconRes == R.drawable.ic_volume_media_bt || iconRes == row.iconRes) 729 ? Events.ICON_STATE_UNMUTE 730 : Events.ICON_STATE_UNKNOWN; 731 row.icon.setContentDescription(ss.name); 732 733 // update slider 734 final boolean enableSlider = !zenMuted; 735 final int vlevel = row.ss.muted && (isRingVibrate || !isRingStream && !zenMuted) ? 0 736 : row.ss.level; 737 updateVolumeRowSliderH(row, enableSlider, vlevel); 738 } 739 updateVolumeRowHeaderVisibleH(VolumeRow row)740 private void updateVolumeRowHeaderVisibleH(VolumeRow row) { 741 final boolean dynamic = row.ss != null && row.ss.dynamic; 742 final boolean showHeaders = mShowHeaders || mExpanded && dynamic; 743 if (row.cachedShowHeaders != showHeaders) { 744 row.cachedShowHeaders = showHeaders; 745 Util.setVisOrGone(row.header, showHeaders); 746 } 747 } 748 updateVolumeRowSliderTintH(VolumeRow row, boolean isActive)749 private void updateVolumeRowSliderTintH(VolumeRow row, boolean isActive) { 750 if (isActive && mExpanded) { 751 row.slider.requestFocus(); 752 } 753 final ColorStateList tint = isActive && row.slider.isEnabled() ? mActiveSliderTint 754 : mInactiveSliderTint; 755 if (tint == row.cachedSliderTint) return; 756 row.cachedSliderTint = tint; 757 row.slider.setProgressTintList(tint); 758 row.slider.setThumbTintList(tint); 759 } 760 updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel)761 private void updateVolumeRowSliderH(VolumeRow row, boolean enable, int vlevel) { 762 row.slider.setEnabled(enable); 763 updateVolumeRowSliderTintH(row, row.stream == mActiveStream); 764 if (row.tracking) { 765 return; // don't update if user is sliding 766 } 767 final int progress = row.slider.getProgress(); 768 final int level = getImpliedLevel(row.slider, progress); 769 final boolean rowVisible = row.view.getVisibility() == View.VISIBLE; 770 final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) 771 < USER_ATTEMPT_GRACE_PERIOD; 772 mHandler.removeMessages(H.RECHECK, row); 773 if (mShowing && rowVisible && inGracePeriod) { 774 if (D.BUG) Log.d(TAG, "inGracePeriod"); 775 mHandler.sendMessageAtTime(mHandler.obtainMessage(H.RECHECK, row), 776 row.userAttempt + USER_ATTEMPT_GRACE_PERIOD); 777 return; // don't update if visible and in grace period 778 } 779 if (vlevel == level) { 780 if (mShowing && rowVisible) { 781 return; // don't clamp if visible 782 } 783 } 784 final int newProgress = vlevel * 100; 785 if (progress != newProgress) { 786 if (mShowing && rowVisible) { 787 // animate! 788 if (row.anim != null && row.anim.isRunning() 789 && row.animTargetProgress == newProgress) { 790 return; // already animating to the target progress 791 } 792 // start/update animation 793 if (row.anim == null) { 794 row.anim = ObjectAnimator.ofInt(row.slider, "progress", progress, newProgress); 795 row.anim.setInterpolator(new DecelerateInterpolator()); 796 } else { 797 row.anim.cancel(); 798 row.anim.setIntValues(progress, newProgress); 799 } 800 row.animTargetProgress = newProgress; 801 row.anim.setDuration(UPDATE_ANIMATION_DURATION); 802 row.anim.start(); 803 } else { 804 // update slider directly to clamped value 805 if (row.anim != null) { 806 row.anim.cancel(); 807 } 808 row.slider.setProgress(newProgress); 809 } 810 } 811 } 812 813 private void recheckH(VolumeRow row) { 814 if (row == null) { 815 if (D.BUG) Log.d(TAG, "recheckH ALL"); 816 trimObsoleteH(); 817 for (VolumeRow r : mRows) { 818 updateVolumeRowH(r); 819 } 820 } else { 821 if (D.BUG) Log.d(TAG, "recheckH " + row.stream); 822 updateVolumeRowH(row); 823 } 824 } 825 826 private void setStreamImportantH(int stream, boolean important) { 827 for (VolumeRow row : mRows) { 828 if (row.stream == stream) { 829 row.important = important; 830 return; 831 } 832 } 833 } 834 835 private void showSafetyWarningH(int flags) { 836 if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 837 || mShowing) { 838 synchronized (mSafetyWarningLock) { 839 if (mSafetyWarning != null) { 840 return; 841 } 842 mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { 843 @Override 844 protected void cleanUp() { 845 synchronized (mSafetyWarningLock) { 846 mSafetyWarning = null; 847 } 848 recheckH(null); 849 } 850 }; 851 mSafetyWarning.show(); 852 } 853 recheckH(null); 854 } 855 rescheduleTimeoutH(); 856 } 857 858 private final VolumeDialogController.Callbacks mControllerCallbackH 859 = new VolumeDialogController.Callbacks() { 860 @Override 861 public void onShowRequested(int reason) { 862 showH(reason); 863 } 864 865 @Override 866 public void onDismissRequested(int reason) { 867 dismissH(reason); 868 } 869 870 @Override 871 public void onScreenOff() { 872 dismissH(Events.DISMISS_REASON_SCREEN_OFF); 873 } 874 875 @Override 876 public void onStateChanged(State state) { 877 onStateChangedH(state); 878 } 879 880 @Override 881 public void onLayoutDirectionChanged(int layoutDirection) { 882 mDialogView.setLayoutDirection(layoutDirection); 883 } 884 885 @Override 886 public void onConfigurationChanged() { 887 updateWindowWidthH(); 888 mSpTexts.update(); 889 mZenFooter.onConfigurationChanged(); 890 } 891 892 @Override 893 public void onShowVibrateHint() { 894 if (mSilentMode) { 895 mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); 896 } 897 } 898 899 @Override 900 public void onShowSilentHint() { 901 if (mSilentMode) { 902 mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); 903 } 904 } 905 906 @Override 907 public void onShowSafetyWarning(int flags) { 908 showSafetyWarningH(flags); 909 } 910 }; 911 912 private final OnClickListener mClickExpand = new OnClickListener() { 913 @Override 914 public void onClick(View v) { 915 if (mExpandButtonAnimationRunning) return; 916 final boolean newExpand = !mExpanded; 917 Events.writeEvent(mContext, Events.EVENT_EXPAND, newExpand); 918 setExpandedH(newExpand); 919 } 920 }; 921 922 private final OnClickListener mClickSettings = new OnClickListener() { 923 @Override 924 public void onClick(View v) { 925 mSettingsButton.postDelayed(new Runnable() { 926 @Override 927 public void run() { 928 Events.writeEvent(mContext, Events.EVENT_SETTINGS_CLICK); 929 if (mCallback != null) { 930 mCallback.onSettingsClicked(); 931 } 932 } 933 }, WAIT_FOR_RIPPLE); 934 } 935 }; 936 937 private final class H extends Handler { 938 private static final int SHOW = 1; 939 private static final int DISMISS = 2; 940 private static final int RECHECK = 3; 941 private static final int RECHECK_ALL = 4; 942 private static final int SET_STREAM_IMPORTANT = 5; 943 private static final int RESCHEDULE_TIMEOUT = 6; 944 private static final int STATE_CHANGED = 7; 945 private static final int UPDATE_BOTTOM_MARGIN = 8; 946 947 public H() { 948 super(Looper.getMainLooper()); 949 } 950 951 @Override 952 public void handleMessage(Message msg) { 953 switch (msg.what) { 954 case SHOW: showH(msg.arg1); break; 955 case DISMISS: dismissH(msg.arg1); break; 956 case RECHECK: recheckH((VolumeRow) msg.obj); break; 957 case RECHECK_ALL: recheckH(null); break; 958 case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; 959 case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; 960 case STATE_CHANGED: onStateChangedH(mState); break; 961 case UPDATE_BOTTOM_MARGIN: updateDialogBottomMarginH(); break; 962 } 963 } 964 } 965 966 private final class CustomDialog extends Dialog { 967 public CustomDialog(Context context) { 968 super(context); 969 } 970 971 @Override 972 public boolean dispatchTouchEvent(MotionEvent ev) { 973 rescheduleTimeoutH(); 974 return super.dispatchTouchEvent(ev); 975 } 976 977 @Override 978 protected void onStop() { 979 super.onStop(); 980 final boolean animating = mMotion.isAnimating(); 981 if (D.BUG) Log.d(TAG, "onStop animating=" + animating); 982 if (animating) { 983 mPendingRecheckAll = true; 984 return; 985 } 986 mHandler.sendEmptyMessage(H.RECHECK_ALL); 987 } 988 989 @Override 990 public boolean onTouchEvent(MotionEvent event) { 991 if (isShowing()) { 992 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 993 dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); 994 return true; 995 } 996 } 997 return false; 998 } 999 } 1000 1001 private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { 1002 private final VolumeRow mRow; 1003 1004 private VolumeSeekBarChangeListener(VolumeRow row) { 1005 mRow = row; 1006 } 1007 1008 @Override 1009 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1010 if (mRow.ss == null) return; 1011 if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) 1012 + " onProgressChanged " + progress + " fromUser=" + fromUser); 1013 if (!fromUser) return; 1014 if (mRow.ss.levelMin > 0) { 1015 final int minProgress = mRow.ss.levelMin * 100; 1016 if (progress < minProgress) { 1017 seekBar.setProgress(minProgress); 1018 } 1019 } 1020 final int userLevel = getImpliedLevel(seekBar, progress); 1021 if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { 1022 mRow.userAttempt = SystemClock.uptimeMillis(); 1023 if (mRow.requestedLevel != userLevel) { 1024 mController.setStreamVolume(mRow.stream, userLevel); 1025 mRow.requestedLevel = userLevel; 1026 Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, 1027 userLevel); 1028 } 1029 } 1030 } 1031 1032 @Override onStartTrackingTouch(SeekBar seekBar)1033 public void onStartTrackingTouch(SeekBar seekBar) { 1034 if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); 1035 mController.setActiveStream(mRow.stream); 1036 mRow.tracking = true; 1037 } 1038 1039 @Override onStopTrackingTouch(SeekBar seekBar)1040 public void onStopTrackingTouch(SeekBar seekBar) { 1041 if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); 1042 mRow.tracking = false; 1043 mRow.userAttempt = SystemClock.uptimeMillis(); 1044 int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); 1045 Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); 1046 if (mRow.ss.level != userLevel) { 1047 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), 1048 USER_ATTEMPT_GRACE_PERIOD); 1049 } 1050 } 1051 } 1052 1053 private final class Accessibility extends AccessibilityDelegate { 1054 private AccessibilityManager mMgr; 1055 private boolean mFeedbackEnabled; 1056 init()1057 public void init() { 1058 mMgr = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 1059 mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 1060 @Override 1061 public void onViewDetachedFromWindow(View v) { 1062 if (D.BUG) Log.d(TAG, "onViewDetachedFromWindow"); 1063 // noop 1064 } 1065 1066 @Override 1067 public void onViewAttachedToWindow(View v) { 1068 if (D.BUG) Log.d(TAG, "onViewAttachedToWindow"); 1069 updateFeedbackEnabled(); 1070 } 1071 }); 1072 mDialogView.setAccessibilityDelegate(this); 1073 mMgr.addAccessibilityStateChangeListener(new AccessibilityStateChangeListener() { 1074 @Override 1075 public void onAccessibilityStateChanged(boolean enabled) { 1076 updateFeedbackEnabled(); 1077 } 1078 }); 1079 updateFeedbackEnabled(); 1080 } 1081 1082 @Override onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event)1083 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 1084 AccessibilityEvent event) { 1085 rescheduleTimeoutH(); 1086 return super.onRequestSendAccessibilityEvent(host, child, event); 1087 } 1088 updateFeedbackEnabled()1089 private void updateFeedbackEnabled() { 1090 mFeedbackEnabled = computeFeedbackEnabled(); 1091 } 1092 computeFeedbackEnabled()1093 private boolean computeFeedbackEnabled() { 1094 // are there any enabled non-generic a11y services? 1095 final List<AccessibilityServiceInfo> services = 1096 mMgr.getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK); 1097 for (AccessibilityServiceInfo asi : services) { 1098 if (asi.feedbackType != 0 && asi.feedbackType != FEEDBACK_GENERIC) { 1099 return true; 1100 } 1101 } 1102 return false; 1103 } 1104 } 1105 1106 private static class VolumeRow { 1107 private View view; 1108 private View space; 1109 private TextView header; 1110 private ImageButton icon; 1111 private SeekBar slider; 1112 private ImageButton settingsButton; 1113 private int stream; 1114 private StreamState ss; 1115 private long userAttempt; // last user-driven slider change 1116 private boolean tracking; // tracking slider touch 1117 private int requestedLevel = -1; // pending user-requested level via progress changed 1118 private int iconRes; 1119 private int iconMuteRes; 1120 private boolean important; 1121 private int cachedIconRes; 1122 private ColorStateList cachedSliderTint; 1123 private int iconState; // from Events 1124 private boolean cachedShowHeaders = VolumePrefs.DEFAULT_SHOW_HEADERS; 1125 private int cachedExpandButtonRes; 1126 private ObjectAnimator anim; // slider progress animation for non-touch-related updates 1127 private int animTargetProgress; 1128 private int lastAudibleLevel = 1; 1129 } 1130 1131 public interface Callback { onSettingsClicked()1132 void onSettingsClicked(); onZenSettingsClicked()1133 void onZenSettingsClicked(); onZenPrioritySettingsClicked()1134 void onZenPrioritySettingsClicked(); 1135 } 1136 } 1137