1 /*
2  * Copyright (C) 2018 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.media.AudioManager.RINGER_MODE_NORMAL;
20 import static android.media.AudioManager.RINGER_MODE_SILENT;
21 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
22 
23 import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER;
24 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
25 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
26 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
27 
28 import static junit.framework.Assert.assertEquals;
29 import static junit.framework.Assert.assertFalse;
30 import static junit.framework.Assert.assertNotNull;
31 import static junit.framework.Assert.assertNotSame;
32 import static junit.framework.Assert.assertTrue;
33 
34 import static org.junit.Assume.assumeNotNull;
35 import static org.mockito.ArgumentMatchers.any;
36 import static org.mockito.ArgumentMatchers.anyInt;
37 import static org.mockito.ArgumentMatchers.eq;
38 import static org.mockito.Mockito.never;
39 import static org.mockito.Mockito.reset;
40 import static org.mockito.Mockito.times;
41 import static org.mockito.Mockito.verify;
42 import static org.mockito.Mockito.when;
43 
44 import android.app.KeyguardManager;
45 import android.content.res.Configuration;
46 import android.graphics.Bitmap;
47 import android.graphics.Canvas;
48 import android.graphics.drawable.Drawable;
49 import android.media.AudioManager;
50 import android.media.AudioSystem;
51 import android.os.SystemClock;
52 import android.platform.test.annotations.DisableFlags;
53 import android.platform.test.annotations.EnableFlags;
54 import android.provider.Settings;
55 import android.testing.TestableLooper;
56 import android.util.Log;
57 import android.view.Gravity;
58 import android.view.InputDevice;
59 import android.view.MotionEvent;
60 import android.view.View;
61 import android.view.ViewGroup;
62 import android.view.accessibility.AccessibilityManager;
63 import android.widget.ImageButton;
64 import android.widget.SeekBar;
65 
66 import androidx.test.core.view.MotionEventBuilder;
67 import androidx.test.ext.junit.runners.AndroidJUnit4;
68 import androidx.test.filters.SmallTest;
69 
70 import com.android.internal.jank.InteractionJankMonitor;
71 import com.android.internal.logging.testing.UiEventLoggerFake;
72 import com.android.systemui.Prefs;
73 import com.android.systemui.SysuiTestCase;
74 import com.android.systemui.animation.AnimatorTestRule;
75 import com.android.systemui.dump.DumpManager;
76 import com.android.systemui.media.dialog.MediaOutputDialogManager;
77 import com.android.systemui.plugins.VolumeDialogController;
78 import com.android.systemui.plugins.VolumeDialogController.State;
79 import com.android.systemui.res.R;
80 import com.android.systemui.statusbar.VibratorHelper;
81 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
82 import com.android.systemui.statusbar.policy.ConfigurationController;
83 import com.android.systemui.statusbar.policy.DevicePostureController;
84 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
85 import com.android.systemui.statusbar.policy.FakeConfigurationController;
86 import com.android.systemui.util.settings.FakeSettings;
87 import com.android.systemui.util.settings.SecureSettings;
88 import com.android.systemui.util.time.FakeSystemClock;
89 import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
90 import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
91 import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
92 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
93 
94 import dagger.Lazy;
95 
96 import junit.framework.Assert;
97 
98 import org.junit.After;
99 import org.junit.Before;
100 import org.junit.Rule;
101 import org.junit.Test;
102 import org.junit.runner.RunWith;
103 import org.mockito.ArgumentCaptor;
104 import org.mockito.Mock;
105 import org.mockito.Mockito;
106 import org.mockito.MockitoAnnotations;
107 
108 import java.util.Arrays;
109 import java.util.function.Predicate;
110 
111 @SmallTest
112 @RunWith(AndroidJUnit4.class)
113 @TestableLooper.RunWithLooper(setAsMainLooper = true)
114 public class VolumeDialogImplTest extends SysuiTestCase {
115     VolumeDialogImpl mDialog;
116     View mActiveRinger;
117     View mDrawerContainer;
118     View mDrawerVibrate;
119     View mDrawerMute;
120     View mDrawerNormal;
121     ViewGroup mDialogRowsView;
122     CaptionsToggleImageButton mODICaptionsIcon;
123     private TestableLooper mTestableLooper;
124     private ConfigurationController mConfigurationController;
125     private int mOriginalOrientation;
126 
127     private static final String TAG = "VolumeDialogImplTest";
128 
129     @Mock
130     VolumeDialogController mVolumeDialogController;
131     @Mock
132     KeyguardManager mKeyguard;
133     @Mock
134     AccessibilityManagerWrapper mAccessibilityMgr;
135     @Mock
136     DeviceProvisionedController mDeviceProvisionedController;
137     @Mock
138     MediaOutputDialogManager mMediaOutputDialogManager;
139     @Mock
140     InteractionJankMonitor mInteractionJankMonitor;
141     @Mock
142     private DumpManager mDumpManager;
143     @Mock CsdWarningDialog mCsdWarningDialog;
144     @Mock
145     DevicePostureController mPostureController;
146     @Mock
147     private Lazy<SecureSettings> mLazySecureSettings;
148     @Mock
149     private VolumePanelNavigationInteractor mVolumePanelNavigationInteractor;
150     @Mock
151     private VolumeNavigator mVolumeNavigator;
152     @Mock
153     private VolumePanelFlag mVolumePanelFlag;
154     @Mock
155     private VolumeDialogInteractor mVolumeDialogInteractor;
156 
157     private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
158             new CsdWarningDialog.Factory() {
159         @Override
160         public CsdWarningDialog create(int warningType, Runnable onCleanup) {
161             return mCsdWarningDialog;
162         }
163     };
164     @Mock
165     private VibratorHelper mVibratorHelper;
166 
167     private int mLongestHideShowAnimationDuration = 250;
168     private FakeSettings mSecureSettings;
169 
170     @Rule
171     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
172 
173    @Before
setup()174     public void setup() throws Exception {
175         MockitoAnnotations.initMocks(this);
176 
177         getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
178 
179         mTestableLooper = TestableLooper.get(this);
180         allowTestableLooperAsMainThread();
181 
182         when(mPostureController.getDevicePosture())
183                 .thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
184 
185         int hideDialogDuration = mContext.getResources()
186                 .getInteger(R.integer.config_dialogHideAnimationDurationMs);
187         int showDialogDuration = mContext.getResources()
188                 .getInteger(R.integer.config_dialogShowAnimationDurationMs);
189 
190         mLongestHideShowAnimationDuration = Math.max(hideDialogDuration, showDialogDuration);
191 
192         mOriginalOrientation = mContext.getResources().getConfiguration().orientation;
193 
194         mConfigurationController = new FakeConfigurationController();
195 
196         mSecureSettings = new FakeSettings();
197 
198         when(mLazySecureSettings.get()).thenReturn(mSecureSettings);
199 
200         when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0});
201 
202         mDialog = new VolumeDialogImpl(
203                 getContext(),
204                 mVolumeDialogController,
205                 mAccessibilityMgr,
206                 mDeviceProvisionedController,
207                 mConfigurationController,
208                 mMediaOutputDialogManager,
209                 mInteractionJankMonitor,
210                 mVolumePanelNavigationInteractor,
211                 mVolumeNavigator,
212                 false,
213                 mCsdWarningDialogFactory,
214                 mPostureController,
215                 mTestableLooper.getLooper(),
216                 mVolumePanelFlag,
217                 mDumpManager,
218                 mLazySecureSettings,
219                 mVibratorHelper,
220                 new FakeSystemClock(),
221                 mVolumeDialogInteractor);
222         mDialog.init(0, null);
223         State state = createShellState();
224         mDialog.onStateChangedH(state);
225 
226         mActiveRinger = mDialog.getDialogView().findViewById(
227                 R.id.volume_new_ringer_active_icon_container);
228         mDrawerContainer = mDialog.getDialogView().findViewById(R.id.volume_drawer_container);
229 
230         // Drawer is not always available, e.g. on TVs
231         if (mDrawerContainer != null) {
232             mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate);
233             mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute);
234             mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal);
235         }
236         mODICaptionsIcon = mDialog.getDialogView().findViewById(R.id.odi_captions_icon);
237 
238         mDialogRowsView = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows);
239 
240         Prefs.putInt(mContext,
241                 Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT,
242                 VolumePrefs.SHOW_RINGER_TOAST_COUNT + 1);
243 
244         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
245     }
246 
assumeHasDrawer()247     private void assumeHasDrawer() {
248         assumeNotNull("Layout does not contain drawer", mDrawerContainer);
249     }
250 
createShellState()251     private State createShellState() {
252         State state = new VolumeDialogController.State();
253         for (int i = AudioManager.STREAM_VOICE_CALL; i <= AudioManager.STREAM_ACCESSIBILITY; i++) {
254             VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState();
255             ss.name = STREAMS.get(i);
256             ss.level = 1;
257             ss.levelMin = 0;
258             ss.levelMax = 25;
259             state.states.append(i, ss);
260         }
261         return state;
262     }
263 
navigateViews(View view, Predicate<View> condition)264     private void navigateViews(View view, Predicate<View> condition) {
265         if (view instanceof ViewGroup) {
266             ViewGroup viewGroup = (ViewGroup) view;
267             for (int i = 0; i < viewGroup.getChildCount(); i++) {
268                 navigateViews(viewGroup.getChildAt(i), condition);
269             }
270         } else {
271             String resourceName = null;
272             try {
273                 resourceName = getContext().getResources().getResourceName(view.getId());
274             } catch (Exception e) {}
275             assertTrue("View " + resourceName != null ? resourceName : view.getId()
276                     + " failed test", condition.test(view));
277         }
278     }
279 
280     @Test
281     @DisableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
addSliderHaptics_withHapticsDisabled_doesNotDeliverOnProgressChangedHaptics()282     public void addSliderHaptics_withHapticsDisabled_doesNotDeliverOnProgressChangedHaptics() {
283         // GIVEN that the slider haptics flag is disabled and we try to add haptics to volume rows
284         mDialog.addSliderHapticsToRows();
285 
286         // WHEN haptics try to be delivered to a volume stream
287         boolean canDeliverHaptics =
288                 mDialog.canDeliverProgressHapticsToStream(AudioSystem.STREAM_MUSIC, true, 50);
289 
290         // THEN the result is that haptics are not successfully delivered
291         assertFalse(canDeliverHaptics);
292     }
293 
294     @Test
295     @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
addSliderHaptics_withHapticsEnabled_canDeliverOnProgressChangedHaptics()296     public void addSliderHaptics_withHapticsEnabled_canDeliverOnProgressChangedHaptics() {
297         // GIVEN that the slider haptics flag is enabled and we try to add haptics to volume rows
298         mDialog.addSliderHapticsToRows();
299 
300         // WHEN haptics try to be delivered to a volume stream
301         boolean canDeliverHaptics =
302                 mDialog.canDeliverProgressHapticsToStream(AudioSystem.STREAM_MUSIC, true, 50);
303 
304         // THEN the result is that haptics are successfully delivered
305         assertTrue(canDeliverHaptics);
306     }
307 
308     @Test
testComputeTimeout()309     public void testComputeTimeout() {
310         Mockito.reset(mAccessibilityMgr);
311         mDialog.rescheduleTimeoutH();
312         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
313                 VolumeDialogImpl.DIALOG_TIMEOUT_MILLIS,
314                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
315     }
316 
317     @Test
testSetTimeoutValue_ComputeTimeout()318     public void testSetTimeoutValue_ComputeTimeout() {
319         mSecureSettings.putInt(Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, 7000);
320         Mockito.reset(mAccessibilityMgr);
321         mDialog.init(0, null);
322         mDialog.rescheduleTimeoutH();
323         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
324                 7000,
325                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
326     }
327 
328     @Test
testComputeTimeout_tooltip()329     public void testComputeTimeout_tooltip() {
330         Mockito.reset(mAccessibilityMgr);
331         mDialog.showCaptionsTooltip();
332         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
333                 VolumeDialogImpl.DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS,
334                 AccessibilityManager.FLAG_CONTENT_CONTROLS
335                 | AccessibilityManager.FLAG_CONTENT_TEXT);
336     }
337 
338     @Test
testComputeTimeout_withHovering()339     public void testComputeTimeout_withHovering() {
340         Mockito.reset(mAccessibilityMgr);
341         View dialog = mDialog.getDialogView();
342         long uptimeMillis = SystemClock.uptimeMillis();
343         MotionEvent event = MotionEvent.obtain(uptimeMillis, uptimeMillis,
344                 MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0);
345         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
346         dialog.dispatchGenericMotionEvent(event);
347         event.recycle();
348         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
349                 VolumeDialogImpl.DIALOG_HOVERING_TIMEOUT_MILLIS,
350                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
351     }
352 
353     @Test
testComputeTimeout_withSafetyWarningOn()354     public void testComputeTimeout_withSafetyWarningOn() {
355         Mockito.reset(mAccessibilityMgr);
356         ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture =
357                 ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class);
358         verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any());
359         VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue();
360 
361         callbacks.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI);
362         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
363                 VolumeDialogImpl.DIALOG_SAFETYWARNING_TIMEOUT_MILLIS,
364                 AccessibilityManager.FLAG_CONTENT_TEXT
365                         | AccessibilityManager.FLAG_CONTENT_CONTROLS);
366     }
367 
368     @Test
testComputeTimeout_standard()369     public void testComputeTimeout_standard() {
370         Mockito.reset(mAccessibilityMgr);
371         mDialog.tryToRemoveCaptionsTooltip();
372         mDialog.rescheduleTimeoutH();
373         verify(mAccessibilityMgr).getRecommendedTimeoutMillis(
374                 VolumeDialogImpl.DIALOG_TIMEOUT_MILLIS,
375                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
376     }
377 
378     @Test
testVibrateOnRingerChangedToVibrate()379     public void testVibrateOnRingerChangedToVibrate() {
380         final State initialSilentState = new State();
381         initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
382 
383         final State vibrateState = new State();
384         vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
385 
386         // change ringer to silent
387         mDialog.onStateChangedH(initialSilentState);
388 
389         // expected: shouldn't call vibrate yet
390         verify(mVolumeDialogController, never()).vibrate(any());
391 
392         // changed ringer to vibrate
393         mDialog.onStateChangedH(vibrateState);
394 
395         // expected: vibrate device
396         verify(mVolumeDialogController).vibrate(any());
397     }
398 
399     @Test
testNoVibrateOnRingerInitialization()400     public void testNoVibrateOnRingerInitialization() {
401         final State initialUnsetState = new State();
402         initialUnsetState.ringerModeInternal = -1;
403 
404         // ringer not initialized yet:
405         mDialog.onStateChangedH(initialUnsetState);
406 
407         final State vibrateState = new State();
408         vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
409 
410         // changed ringer to vibrate
411         mDialog.onStateChangedH(vibrateState);
412 
413         // shouldn't call vibrate
414         verify(mVolumeDialogController, never()).vibrate(any());
415     }
416 
417     @Test
testSelectVibrateFromDrawer()418     public void testSelectVibrateFromDrawer() {
419         assumeHasDrawer();
420 
421         final State initialUnsetState = new State();
422         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
423         mDialog.onStateChangedH(initialUnsetState);
424 
425         mActiveRinger.performClick();
426         mDrawerVibrate.performClick();
427 
428         // Make sure we've actually changed the ringer mode.
429         verify(mVolumeDialogController, times(1)).setRingerMode(
430                 AudioManager.RINGER_MODE_VIBRATE, false);
431     }
432 
433     @Test
testSelectMuteFromDrawer()434     public void testSelectMuteFromDrawer() {
435         assumeHasDrawer();
436 
437         final State initialUnsetState = new State();
438         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
439         mDialog.onStateChangedH(initialUnsetState);
440 
441         mActiveRinger.performClick();
442         mDrawerMute.performClick();
443 
444         // Make sure we've actually changed the ringer mode.
445         verify(mVolumeDialogController, times(1)).setRingerMode(
446                 AudioManager.RINGER_MODE_SILENT, false);
447     }
448 
449     @Test
testSelectNormalFromDrawer()450     public void testSelectNormalFromDrawer() {
451         assumeHasDrawer();
452 
453         final State initialUnsetState = new State();
454         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
455         mDialog.onStateChangedH(initialUnsetState);
456 
457         mActiveRinger.performClick();
458         mDrawerNormal.performClick();
459 
460         // Make sure we've actually changed the ringer mode.
461         verify(mVolumeDialogController, times(1)).setRingerMode(
462                 AudioManager.RINGER_MODE_NORMAL, false);
463     }
464 
465     /**
466      * Ideally we would look at the ringer ImageView and check its assigned drawable id, but that
467      * API does not exist. So we do the next best thing; we check the cached icon id.
468      */
469     @Test
notificationVolumeSeparated_theRingerIconChangesToSpeakerIcon()470     public void notificationVolumeSeparated_theRingerIconChangesToSpeakerIcon() {
471         // already separated. assert icon is new based on res id
472         assertEquals(mDialog.mVolumeRingerIconDrawableId,
473                 R.drawable.ic_speaker_on);
474         assertEquals(mDialog.mVolumeRingerMuteIconDrawableId,
475                 R.drawable.ic_speaker_mute);
476     }
477 
478     @Test
testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation()479     public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() {
480         mDialog.dismissH(DISMISS_REASON_UNKNOWN);
481         // notifyVisible(false) should not be called immediately but only after the dismiss
482         // animation has ended.
483         verify(mVolumeDialogController, times(0)).notifyVisible(false);
484         mDialog.getDialogView().animate().cancel();
485     }
486 
487     @Test
showCsdWarning_dialogShown()488     public void showCsdWarning_dialogShown() {
489         mDialog.showCsdWarningH(AudioManager.CSD_WARNING_DOSE_REACHED_1X,
490                 CsdWarningDialog.NO_ACTION_TIMEOUT_MS);
491 
492         verify(mCsdWarningDialog).show();
493     }
494 
495     @Test
ifPortraitHalfOpen_drawVerticallyTop()496     public void ifPortraitHalfOpen_drawVerticallyTop() {
497         mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
498         mTestableLooper.processAllMessages(); // let dismiss() finish
499 
500         setOrientation(Configuration.ORIENTATION_PORTRAIT);
501 
502         // Call show() to trigger layout updates before verifying position
503         mDialog.show(SHOW_REASON_UNKNOWN);
504         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
505 
506         int gravity = mDialog.getWindowGravity();
507         assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK);
508     }
509 
510     @Test
ifPortraitAndOpen_drawCenterVertically()511     public void ifPortraitAndOpen_drawCenterVertically() {
512         mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
513         mTestableLooper.processAllMessages(); // let dismiss() finish
514 
515         setOrientation(Configuration.ORIENTATION_PORTRAIT);
516 
517         mDialog.show(SHOW_REASON_UNKNOWN);
518         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
519 
520         int gravity = mDialog.getWindowGravity();
521         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
522     }
523 
524     @Test
ifLandscapeAndHalfOpen_drawCenterVertically()525     public void ifLandscapeAndHalfOpen_drawCenterVertically() {
526         mDialog.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
527         mTestableLooper.processAllMessages(); // let dismiss() finish
528 
529         setOrientation(Configuration.ORIENTATION_LANDSCAPE);
530 
531         mDialog.show(SHOW_REASON_UNKNOWN);
532         mTestableLooper.processAllMessages(); // let show() finish before assessing its side-effect
533 
534         int gravity = mDialog.getWindowGravity();
535         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
536     }
537 
538     @Test
dialogInit_addsPostureControllerCallback()539     public void dialogInit_addsPostureControllerCallback() {
540         // init is already called in setup
541         verify(mPostureController).addCallback(any());
542     }
543 
544     @Test
dialogDestroy_removesPostureControllerCallback()545     public void dialogDestroy_removesPostureControllerCallback() {
546         verify(mPostureController, never()).removeCallback(any());
547         mDialog.destroy();
548         verify(mPostureController).removeCallback(any());
549     }
550 
setOrientation(int orientation)551     private void setOrientation(int orientation) {
552         Configuration config = new Configuration();
553         config.orientation = orientation;
554         if (mConfigurationController != null) {
555             mConfigurationController.onConfigurationChanged(config);
556         }
557     }
558 
559     private enum RingerDrawerState {INIT, OPEN, CLOSE}
560 
561     @Test
ringerModeNormal_ringerContainerDescribesItsState()562     public void ringerModeNormal_ringerContainerDescribesItsState() {
563         assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.INIT);
564     }
565 
566     @Test
ringerModeSilent_ringerContainerDescribesItsState()567     public void ringerModeSilent_ringerContainerDescribesItsState() {
568         assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.INIT);
569     }
570 
571     @Test
ringerModeVibrate_ringerContainerDescribesItsState()572     public void ringerModeVibrate_ringerContainerDescribesItsState() {
573         assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.INIT);
574     }
575 
576     @Test
ringerModeNormal_openDrawer_ringerContainerDescribesItsState()577     public void ringerModeNormal_openDrawer_ringerContainerDescribesItsState() {
578         assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.OPEN);
579     }
580 
581     @Test
ringerModeSilent_openDrawer_ringerContainerDescribesItsState()582     public void ringerModeSilent_openDrawer_ringerContainerDescribesItsState() {
583         assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.OPEN);
584     }
585 
586     @Test
ringerModeVibrate_openDrawer_ringerContainerDescribesItsState()587     public void ringerModeVibrate_openDrawer_ringerContainerDescribesItsState() {
588         assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.OPEN);
589     }
590 
591     @Test
ringerModeNormal_closeDrawer_ringerContainerDescribesItsState()592     public void ringerModeNormal_closeDrawer_ringerContainerDescribesItsState() {
593         assertRingerContainerDescribesItsState(RINGER_MODE_NORMAL, RingerDrawerState.CLOSE);
594     }
595 
596     @Test
ringerModeSilent_closeDrawer_ringerContainerDescribesItsState()597     public void ringerModeSilent_closeDrawer_ringerContainerDescribesItsState() {
598         assertRingerContainerDescribesItsState(RINGER_MODE_SILENT, RingerDrawerState.CLOSE);
599     }
600 
601     @Test
ringerModeVibrate_closeDrawer_ringerContainerDescribesItsState()602     public void ringerModeVibrate_closeDrawer_ringerContainerDescribesItsState() {
603         assertRingerContainerDescribesItsState(RINGER_MODE_VIBRATE, RingerDrawerState.CLOSE);
604     }
605 
606     @Test
testOnCaptionEnabledStateChanged_checkBeforeSwitchTrue_setCaptionsEnabledState()607     public void testOnCaptionEnabledStateChanged_checkBeforeSwitchTrue_setCaptionsEnabledState() {
608         ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture =
609                 ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class);
610         verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any());
611         VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue();
612 
613         callbacks.onCaptionEnabledStateChanged(true, true);
614         verify(mVolumeDialogController).setCaptionsEnabledState(eq(false));
615     }
616 
617     @Test
testOnCaptionEnabledStateChanged_checkBeforeSwitchFalse_getCaptionsEnabledTrue()618     public void testOnCaptionEnabledStateChanged_checkBeforeSwitchFalse_getCaptionsEnabledTrue() {
619         ArgumentCaptor<VolumeDialogController.Callbacks> controllerCallbackCapture =
620                 ArgumentCaptor.forClass(VolumeDialogController.Callbacks.class);
621         verify(mVolumeDialogController).addCallback(controllerCallbackCapture.capture(), any());
622         VolumeDialogController.Callbacks callbacks = controllerCallbackCapture.getValue();
623 
624         callbacks.onCaptionEnabledStateChanged(true, false);
625         assertTrue(mODICaptionsIcon.getCaptionsEnabled());
626     }
627 
628     /**
629      * The content description should include ringer state, and the correct one.
630      */
assertRingerContainerDescribesItsState(int ringerMode, RingerDrawerState drawerState)631     private void assertRingerContainerDescribesItsState(int ringerMode,
632             RingerDrawerState drawerState) {
633         assumeHasDrawer();
634 
635         State state = createShellState();
636         state.ringerModeInternal = ringerMode;
637         mDialog.onStateChangedH(state);
638 
639         mDialog.show(SHOW_REASON_UNKNOWN);
640 
641         if (drawerState != RingerDrawerState.INIT) {
642             // in both cases we first open the drawer
643             mDialog.toggleRingerDrawer(true);
644 
645             if (drawerState == RingerDrawerState.CLOSE) {
646                 mDialog.toggleRingerDrawer(false);
647             }
648         }
649 
650         String ringerContainerDescription = mDialog.getSelectedRingerContainerDescription();
651         assumeNotNull(ringerContainerDescription);
652 
653         String ringerDescription = mContext.getString(
654                 mDialog.getStringDescriptionResourceForRingerMode(ringerMode));
655 
656         if (drawerState == RingerDrawerState.OPEN) {
657             assertEquals(ringerDescription, ringerContainerDescription);
658         } else {
659             assertNotSame(ringerDescription, ringerContainerDescription);
660             assertTrue(ringerContainerDescription.startsWith(ringerDescription));
661         }
662     }
663 
664     /**
665      * The click should be a single tap, thus we inject a down and an up event.
666      */
667     @Test
clickCaptionsButton_logsUiEvent()668     public void clickCaptionsButton_logsUiEvent() {
669         UiEventLoggerFake logger = new UiEventLoggerFake();
670         Events.sUiEventLogger = logger;
671         MotionEvent down = MotionEventBuilder.newBuilder()
672                 .setAction(MotionEvent.ACTION_DOWN).build();
673         MotionEvent up = MotionEventBuilder.newBuilder()
674                 .setAction(MotionEvent.ACTION_UP).build();
675 
676         mODICaptionsIcon.onTouchEvent(down);
677         mODICaptionsIcon.onTouchEvent(up);
678         mTestableLooper.moveTimeForward(300); // to confirm it was only a single tap
679         mTestableLooper.processAllMessages();
680 
681         boolean foundCaptionLog = false;
682         for (UiEventLoggerFake.FakeUiEvent event : logger.getLogs()) {
683             if (event.eventId
684                     == Events.VolumeDialogEvent.VOLUME_DIALOG_ODI_CAPTIONS_CLICKED.getId()) {
685                 foundCaptionLog = true;
686                 break;
687             }
688         }
689         Assert.assertTrue("Did not log the captions button click.", foundCaptionLog);
690     }
691 
692     /**
693      * Pressing the small x button at top right dismisses the captions tooltip.
694      */
695     @Test
dismissCaptionsTooltip_logsUiEvent()696     public void dismissCaptionsTooltip_logsUiEvent() {
697         UiEventLoggerFake logger = new UiEventLoggerFake();
698         Events.sUiEventLogger = logger;
699         mDialog.showCaptionsTooltip();
700         assumeNotNull(mDialog.mODICaptionsTooltipView);
701         View dismissButton = mDialog.mODICaptionsTooltipView.findViewById(R.id.dismiss);
702 
703         dismissButton.performClick();
704 
705         boolean foundCaptionLog = false;
706         for (UiEventLoggerFake.FakeUiEvent event : logger.getLogs()) {
707             if (event.eventId
708                     == Events.VolumeDialogEvent.VOLUME_DIALOG_ODI_CAPTIONS_TOOLTIP_CLICKED.getId()
709             ) {
710                 foundCaptionLog = true;
711                 break;
712             }
713         }
714         Assert.assertTrue("Did not log the captions tooltip dismiss button click.",
715                 foundCaptionLog);
716     }
717 
718     @Test
volumeSliderTracksTouch_logsStartAndStopTrackingUiEvents()719     public void volumeSliderTracksTouch_logsStartAndStopTrackingUiEvents() {
720         UiEventLoggerFake logger = new UiEventLoggerFake();
721         Events.sUiEventLogger = logger;
722 
723         mDialog.show(SHOW_REASON_UNKNOWN);
724         mTestableLooper.processAllMessages();
725 
726         MotionEvent down = MotionEventBuilder.newBuilder()
727                 .setAction(MotionEvent.ACTION_DOWN).build();
728         MotionEvent up = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build();
729 
730         SeekBar slider =
731                 mDialogRowsView.getChildAt(0).findViewById(R.id.volume_row_slider);
732         slider.onTouchEvent(down);
733         slider.onTouchEvent(up);
734         mTestableLooper.moveTimeForward(300);
735         mTestableLooper.processAllMessages();
736 
737         boolean foundStartTrackingTouch = false;
738         boolean foundStopTrackingTouch = false;
739         for (UiEventLoggerFake.FakeUiEvent event : logger.getLogs()) {
740             if (event.eventId
741                     == Events.VolumeDialogEvent.VOLUME_DIALOG_SLIDER_STARTED_TRACKING_TOUCH.getId()
742             ) {
743                 foundStartTrackingTouch = true;
744             }
745             if (event.eventId
746                     == Events.VolumeDialogEvent.VOLUME_DIALOG_SLIDER_STOPPED_TRACKING_TOUCH.getId()
747             ) {
748                 foundStopTrackingTouch = true;
749             }
750         }
751         Assert.assertTrue("Did not log the event of start tracking touch.",
752                 foundStartTrackingTouch);
753         Assert.assertTrue("Did not log the event of stop tracking touch.",
754                 foundStopTrackingTouch);
755     }
756 
757     @Test
turnOnDnD_volumeSliderIconChangesToDnd()758     public void turnOnDnD_volumeSliderIconChangesToDnd() {
759         State state = createShellState();
760         state.zenMode = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
761 
762         mDialog.onStateChangedH(state);
763         mTestableLooper.processAllMessages();
764 
765         boolean foundDnDIcon = findDndIconAmongVolumeRows();
766         assertTrue(foundDnDIcon);
767     }
768 
769     @Test
turnOffDnD_volumeSliderIconIsNotDnd()770     public void turnOffDnD_volumeSliderIconIsNotDnd() {
771         State state = createShellState();
772         state.zenMode = Settings.Global.ZEN_MODE_OFF;
773 
774         mDialog.onStateChangedH(state);
775         mTestableLooper.processAllMessages();
776 
777         boolean foundDnDIcon = findDndIconAmongVolumeRows();
778         assertFalse(foundDnDIcon);
779     }
780 
781     @Test
testInteractor_onShow()782     public void testInteractor_onShow() {
783         mDialog.show(SHOW_REASON_UNKNOWN);
784         mTestableLooper.processAllMessages();
785 
786         verify(mVolumeDialogInteractor).onDialogShown();
787         verify(mVolumeDialogInteractor).onDialogDismissed(); // dismiss by timeout
788     }
789 
790     /**
791      * @return true if at least one volume row has the DND icon
792      */
findDndIconAmongVolumeRows()793     private boolean findDndIconAmongVolumeRows() {
794         ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows);
795         assumeNotNull(volumeDialogRows);
796         Drawable expected =  getContext().getDrawable(com.android.internal.R.drawable.ic_qs_dnd);
797         boolean foundDnDIcon = false;
798         final int rowCount = volumeDialogRows.getChildCount();
799         // we don't make assumptions about the position of the dnd row
800         for (int i = 0; i < rowCount && !foundDnDIcon; i++) {
801             View volumeRow = volumeDialogRows.getChildAt(i);
802             ImageButton rowIcon = volumeRow.findViewById(R.id.volume_row_icon);
803             assertNotNull(rowIcon);
804 
805             // VolumeDialogImpl changes tint and alpha in a private method, so we clear those here.
806             rowIcon.setImageTintList(null);
807             rowIcon.setAlpha(0xFF);
808 
809             Drawable actual = rowIcon.getDrawable();
810             foundDnDIcon |= areDrawablesEqual(expected, actual);
811         }
812         return foundDnDIcon;
813     }
814 
areDrawablesEqual(Drawable drawable1, Drawable drawable2)815     private boolean areDrawablesEqual(Drawable drawable1, Drawable drawable2) {
816         int size = 100;
817         Bitmap bm1 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
818         Bitmap bm2 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
819 
820         Canvas canvas1 = new Canvas(bm1);
821         Canvas canvas2 = new Canvas(bm2);
822 
823         drawable1.setBounds(0, 0, size, size);
824         drawable2.setBounds(0, 0, size, size);
825 
826         drawable1.draw(canvas1);
827         drawable2.draw(canvas2);
828 
829         boolean areBitmapsEqual = areBitmapsEqual(bm1, bm2);
830         bm1.recycle();
831         bm2.recycle();
832         return areBitmapsEqual;
833     }
834 
areBitmapsEqual(Bitmap a, Bitmap b)835     private boolean areBitmapsEqual(Bitmap a, Bitmap b) {
836         if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) return false;
837         int w = a.getWidth();
838         int h = a.getHeight();
839         int[] aPix = new int[w * h];
840         int[] bPix = new int[w * h];
841         a.getPixels(aPix, 0, w, 0, 0, w, h);
842         b.getPixels(bPix, 0, w, 0, 0, w, h);
843         return Arrays.equals(aPix, bPix);
844     }
845 
846     @After
teardown()847     public void teardown() {
848         // Detailed logs to track down timeout issues in b/299491332
849         Log.d(TAG, "teardown: entered");
850         setOrientation(mOriginalOrientation);
851         Log.d(TAG, "teardown: after setOrientation");
852         // Unclear why we used to do this, and it seems to be a source of flakes
853         // mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
854         Log.d(TAG, "teardown: skipped advanceTimeBy");
855         mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration);
856         Log.d(TAG, "teardown: after moveTimeForward");
857         mTestableLooper.processAllMessages();
858         Log.d(TAG, "teardown: after processAllMessages");
859         reset(mPostureController);
860         Log.d(TAG, "teardown: after reset");
861         cleanUp(mDialog);
862         Log.d(TAG, "teardown: after cleanUp");
863     }
864 
cleanUp(VolumeDialogImpl dialog)865     private void cleanUp(VolumeDialogImpl dialog) {
866         if (dialog != null) {
867             dialog.clearInternalHandlerAfterTest();
868         }
869     }
870 
871 /*
872     @Test
873     public void testContentDescriptions() {
874         mDialog.show(SHOW_REASON_UNKNOWN);
875         ViewGroup dialog = mDialog.getDialogView();
876 
877         navigateViews(dialog, view -> {
878             if (view instanceof ImageView) {
879                 return !TextUtils.isEmpty(view.getContentDescription());
880             } else {
881                 return true;
882             }
883         });
884 
885         mDialog.dismiss(DISMISS_REASON_UNKNOWN);
886     }
887 
888     @Test
889     public void testNoDuplicationOfParentState() {
890         mDialog.show(SHOW_REASON_UNKNOWN);
891         ViewGroup dialog = mDialog.getDialogView();
892 
893         navigateViews(dialog, view -> !view.isDuplicateParentStateEnabled());
894 
895         mDialog.dismiss(DISMISS_REASON_UNKNOWN);
896     }
897 
898     @Test
899     public void testNoClickableViewGroups() {
900         mDialog.show(SHOW_REASON_UNKNOWN);
901         ViewGroup dialog = mDialog.getDialogView();
902 
903         navigateViews(dialog, view -> {
904             if (view instanceof ViewGroup) {
905                 return !view.isClickable();
906             } else {
907                 return true;
908             }
909         });
910 
911         mDialog.dismiss(DISMISS_REASON_UNKNOWN);
912     }
913 
914     @Test
915     public void testTristateToggle_withVibrator() {
916         when(mController.hasVibrator()).thenReturn(true);
917 
918         State state = createShellState();
919         state.ringerModeInternal = RINGER_MODE_NORMAL;
920         mDialog.onStateChangedH(state);
921 
922         mDialog.show(SHOW_REASON_UNKNOWN);
923         ViewGroup dialog = mDialog.getDialogView();
924 
925         // click once, verify updates to vibrate
926         dialog.findViewById(R.id.ringer_icon).performClick();
927         verify(mController, times(1)).setRingerMode(RINGER_MODE_VIBRATE, false);
928 
929         // fake the update back to the dialog with the new ringer mode
930         state = createShellState();
931         state.ringerModeInternal = RINGER_MODE_VIBRATE;
932         mDialog.onStateChangedH(state);
933 
934         // click once, verify updates to silent
935         dialog.findViewById(R.id.ringer_icon).performClick();
936         verify(mController, times(1)).setRingerMode(RINGER_MODE_SILENT, false);
937         verify(mController, times(1)).setStreamVolume(STREAM_RING, 0);
938 
939         // fake the update back to the dialog with the new ringer mode
940         state = createShellState();
941         state.states.get(STREAM_RING).level = 0;
942         state.ringerModeInternal = RINGER_MODE_SILENT;
943         mDialog.onStateChangedH(state);
944 
945         // click once, verify updates to normal
946         dialog.findViewById(R.id.ringer_icon).performClick();
947         verify(mController, times(1)).setRingerMode(RINGER_MODE_NORMAL, false);
948         verify(mController, times(1)).setStreamVolume(STREAM_RING, 0);
949     }
950 
951     @Test
952     public void testTristateToggle_withoutVibrator() {
953         when(mController.hasVibrator()).thenReturn(false);
954 
955         State state = createShellState();
956         state.ringerModeInternal = RINGER_MODE_NORMAL;
957         mDialog.onStateChangedH(state);
958 
959         mDialog.show(SHOW_REASON_UNKNOWN);
960         ViewGroup dialog = mDialog.getDialogView();
961 
962         // click once, verify updates to silent
963         dialog.findViewById(R.id.ringer_icon).performClick();
964         verify(mController, times(1)).setRingerMode(RINGER_MODE_SILENT, false);
965         verify(mController, times(1)).setStreamVolume(STREAM_RING, 0);
966 
967         // fake the update back to the dialog with the new ringer mode
968         state = createShellState();
969         state.states.get(STREAM_RING).level = 0;
970         state.ringerModeInternal = RINGER_MODE_SILENT;
971         mDialog.onStateChangedH(state);
972 
973         // click once, verify updates to normal
974         dialog.findViewById(R.id.ringer_icon).performClick();
975         verify(mController, times(1)).setRingerMode(RINGER_MODE_NORMAL, false);
976         verify(mController, times(1)).setStreamVolume(STREAM_RING, 0);
977     }
978     */
979 }
980