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