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