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