1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.quickstep;
18 
19 import static androidx.test.InstrumentationRegistry.getInstrumentation;
20 
21 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ALL;
22 import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON;
23 import static com.android.quickstep.NavigationModeSwitchRule.Mode.TWO_BUTTON;
24 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
25 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_2BUTTON_OVERLAY;
26 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY;
27 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY;
28 
29 import android.content.Context;
30 import android.content.pm.PackageManager;
31 import android.util.Log;
32 
33 import androidx.test.uiautomator.UiDevice;
34 
35 import com.android.launcher3.tapl.LauncherInstrumentation;
36 import com.android.launcher3.tapl.TestHelpers;
37 import com.android.launcher3.ui.AbstractLauncherUiTest;
38 import com.android.launcher3.util.Wait;
39 import com.android.launcher3.util.rule.FailureWatcher;
40 import com.android.systemui.shared.system.QuickStepContract;
41 
42 import org.junit.rules.TestRule;
43 import org.junit.runner.Description;
44 import org.junit.runners.model.Statement;
45 
46 import java.lang.annotation.ElementType;
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.lang.annotation.Target;
50 import java.util.concurrent.CountDownLatch;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * Test rule that allows executing a test with Quickstep on and then Quickstep off.
55  * The test should be annotated with @QuickstepOnOff.
56  */
57 public class NavigationModeSwitchRule implements TestRule {
58 
59     static final String TAG = "QuickStepOnOffRule";
60 
61     public static final int WAIT_TIME_MS = 10000;
62 
63     public enum Mode {
64         THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL
65     }
66 
67     // Annotation for tests that need to be run with quickstep enabled and disabled.
68     @Retention(RetentionPolicy.RUNTIME)
69     @Target(ElementType.METHOD)
70     public @interface NavigationModeSwitch {
mode()71         Mode mode() default ALL;
72     }
73 
74     private final LauncherInstrumentation mLauncher;
75 
76     static final SysUINavigationMode SYS_UI_NAVIGATION_MODE =
77             SysUINavigationMode.INSTANCE.get(getInstrumentation().getTargetContext());
78 
NavigationModeSwitchRule(LauncherInstrumentation launcher)79     public NavigationModeSwitchRule(LauncherInstrumentation launcher) {
80         mLauncher = launcher;
81     }
82 
83     @Override
apply(Statement base, Description description)84     public Statement apply(Statement base, Description description) {
85         if (TestHelpers.isInLauncherProcess() &&
86                 description.getAnnotation(NavigationModeSwitch.class) != null) {
87             Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode();
88             return new Statement() {
89                 @Override
90                 public void evaluate() throws Throwable {
91                     mLauncher.enableDebugTracing();
92                     final Context context = getInstrumentation().getContext();
93                     final int currentInteractionMode =
94                             LauncherInstrumentation.getCurrentInteractionMode(context);
95                     final String prevOverlayPkg = getCurrentOverlayPackage(currentInteractionMode);
96                     final LauncherInstrumentation.NavigationModel originalMode =
97                             mLauncher.getNavigationModel();
98                     try {
99                         if (mode == ZERO_BUTTON || mode == ALL) {
100                             evaluateWithZeroButtons();
101                         }
102                         if (mode == TWO_BUTTON || mode == ALL) {
103                             evaluateWithTwoButtons();
104                         }
105                         if (mode == THREE_BUTTON || mode == ALL) {
106                             evaluateWithThreeButtons();
107                         }
108                     } catch (Throwable e) {
109                         Log.e(TAG, "Error", e);
110                         throw e;
111                     } finally {
112                         Log.d(TAG, "In Finally block");
113                         assertTrue(mLauncher, "Couldn't set overlay",
114                                 setActiveOverlay(mLauncher, prevOverlayPkg, originalMode,
115                                         description), description);
116                     }
117                 }
118 
119                 private void evaluateWithThreeButtons() throws Throwable {
120                     if (setActiveOverlay(mLauncher, NAV_BAR_MODE_3BUTTON_OVERLAY,
121                             LauncherInstrumentation.NavigationModel.THREE_BUTTON, description)) {
122                         base.evaluate();
123                     }
124                 }
125 
126                 private void evaluateWithTwoButtons() throws Throwable {
127                     if (setActiveOverlay(mLauncher, NAV_BAR_MODE_2BUTTON_OVERLAY,
128                             LauncherInstrumentation.NavigationModel.TWO_BUTTON, description)) {
129                         base.evaluate();
130                     }
131                 }
132 
133                 private void evaluateWithZeroButtons() throws Throwable {
134                     if (setActiveOverlay(mLauncher, NAV_BAR_MODE_GESTURAL_OVERLAY,
135                             LauncherInstrumentation.NavigationModel.ZERO_BUTTON, description)) {
136                         base.evaluate();
137                     }
138                 }
139             };
140         } else {
141             return base;
142         }
143     }
144 
145     public static String getCurrentOverlayPackage(int currentInteractionMode) {
146         return QuickStepContract.isGesturalMode(currentInteractionMode)
147                 ? NAV_BAR_MODE_GESTURAL_OVERLAY
148                 : QuickStepContract.isSwipeUpMode(currentInteractionMode)
149                         ? NAV_BAR_MODE_2BUTTON_OVERLAY
150                         : NAV_BAR_MODE_3BUTTON_OVERLAY;
151     }
152 
153     private static LauncherInstrumentation.NavigationModel currentSysUiNavigationMode() {
154         return LauncherInstrumentation.getNavigationModel(
155                 SysUINavigationMode.getMode(
156                         getInstrumentation().
157                                 getTargetContext()).
158                         resValue);
159     }
160 
161     public static boolean setActiveOverlay(LauncherInstrumentation launcher, String overlayPackage,
162             LauncherInstrumentation.NavigationModel expectedMode, Description description)
163             throws Exception {
164         if (!packageExists(overlayPackage)) {
165             Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
166             return false;
167         }
168 
169         Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
170         UiDevice.getInstance(getInstrumentation()).executeShellCommand(
171                 "cmd overlay enable-exclusive --category " + overlayPackage);
172 
173         if (currentSysUiNavigationMode() != expectedMode) {
174             final CountDownLatch latch = new CountDownLatch(1);
175             final Context targetContext = getInstrumentation().getTargetContext();
176             final SysUINavigationMode.NavigationModeChangeListener listener =
177                     newMode -> {
178                         if (LauncherInstrumentation.getNavigationModel(newMode.resValue)
179                                 == expectedMode) {
180                             latch.countDown();
181                         }
182                     };
183             targetContext.getMainExecutor().execute(() ->
184                     SYS_UI_NAVIGATION_MODE.addModeChangeListener(listener));
185             latch.await(60, TimeUnit.SECONDS);
186             targetContext.getMainExecutor().execute(() ->
187                     SYS_UI_NAVIGATION_MODE.removeModeChangeListener(listener));
188 
189             assertTrue(launcher, "Navigation mode didn't change to " + expectedMode,
190                     currentSysUiNavigationMode() == expectedMode, description);
191 
192         }
193 
194         Wait.atMost("Couldn't switch to " + overlayPackage,
195                 () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
196 
197         Wait.atMost(() -> "Switching nav mode: "
198                         + launcher.getNavigationModeMismatchError(false),
199                 () -> launcher.getNavigationModeMismatchError(false) == null,
200                 WAIT_TIME_MS, launcher);
201         AbstractLauncherUiTest.checkDetectedLeaks(launcher);
202         return true;
203     }
204 
205     private static boolean packageExists(String packageName) {
206         try {
207             PackageManager pm = getInstrumentation().getContext().getPackageManager();
208             if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) {
209                 return false;
210             }
211         } catch (PackageManager.NameNotFoundException e) {
212             return false;
213         }
214         return true;
215     }
216 
217     private static void assertTrue(LauncherInstrumentation launcher, String message,
218             boolean condition, Description description) {
219         launcher.checkForAnomaly(true, true);
220         if (!condition) {
221             final AssertionError assertionError = new AssertionError(message);
222             if (description != null) {
223                 FailureWatcher.onError(launcher.getDevice(), description, assertionError);
224             }
225             throw assertionError;
226         }
227     }
228 }
229