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