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.util.rule.FailureWatcher;
38 import com.android.systemui.shared.system.QuickStepContract;
39 
40 import org.junit.rules.TestRule;
41 import org.junit.runner.Description;
42 import org.junit.runners.model.Statement;
43 
44 import java.lang.annotation.ElementType;
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.lang.annotation.Target;
48 import java.util.concurrent.CountDownLatch;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * Test rule that allows executing a test with Quickstep on and then Quickstep off.
53  * The test should be annotated with @QuickstepOnOff.
54  */
55 public class NavigationModeSwitchRule implements TestRule {
56 
57     static final String TAG = "QuickStepOnOffRule";
58 
59     public enum Mode {
60         THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL
61     }
62 
63     // Annotation for tests that need to be run with quickstep enabled and disabled.
64     @Retention(RetentionPolicy.RUNTIME)
65     @Target(ElementType.METHOD)
66     public @interface NavigationModeSwitch {
mode()67         Mode mode() default ALL;
68     }
69 
70     private final LauncherInstrumentation mLauncher;
71 
NavigationModeSwitchRule(LauncherInstrumentation launcher)72     public NavigationModeSwitchRule(LauncherInstrumentation launcher) {
73         mLauncher = launcher;
74     }
75 
76     @Override
apply(Statement base, Description description)77     public Statement apply(Statement base, Description description) {
78         if (TestHelpers.isInLauncherProcess() &&
79                 description.getAnnotation(NavigationModeSwitch.class) != null) {
80             Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode();
81             return new Statement() {
82                 private void assertTrue(String message, boolean condition) {
83                     if(!condition) {
84                         final AssertionError assertionError = new AssertionError(message);
85                         FailureWatcher.onError(mLauncher.getDevice(), description, assertionError);
86                         throw assertionError;
87                     }
88                 }
89 
90                 @Override
91                 public void evaluate() throws Throwable {
92                     mLauncher.enableDebugTracing();
93                     final Context context = getInstrumentation().getContext();
94                     final int currentInteractionMode =
95                             LauncherInstrumentation.getCurrentInteractionMode(context);
96                     final String prevOverlayPkg =
97                             QuickStepContract.isGesturalMode(currentInteractionMode)
98                                     ? NAV_BAR_MODE_GESTURAL_OVERLAY
99                                     : QuickStepContract.isSwipeUpMode(currentInteractionMode)
100                                             ? NAV_BAR_MODE_2BUTTON_OVERLAY
101                                             : NAV_BAR_MODE_3BUTTON_OVERLAY;
102                     final LauncherInstrumentation.NavigationModel originalMode =
103                             mLauncher.getNavigationModel();
104                     try {
105                         if (mode == ZERO_BUTTON || mode == ALL) {
106                             evaluateWithZeroButtons();
107                         }
108                         if (mode == TWO_BUTTON || mode == ALL) {
109                             evaluateWithTwoButtons();
110                         }
111                         if (mode == THREE_BUTTON || mode == ALL) {
112                             evaluateWithThreeButtons();
113                         }
114                     } catch (Exception e) {
115                         Log.e(TAG, "Exception", e);
116                         throw e;
117                     } finally {
118                         assertTrue("Couldn't set overlay",
119                                 setActiveOverlay(prevOverlayPkg, originalMode));
120                     }
121                 }
122 
123                 private void evaluateWithThreeButtons() throws Throwable {
124                     if (setActiveOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY,
125                             LauncherInstrumentation.NavigationModel.THREE_BUTTON)) {
126                         base.evaluate();
127                     }
128                 }
129 
130                 private void evaluateWithTwoButtons() throws Throwable {
131                     if (setActiveOverlay(NAV_BAR_MODE_2BUTTON_OVERLAY,
132                             LauncherInstrumentation.NavigationModel.TWO_BUTTON)) {
133                         base.evaluate();
134                     }
135                 }
136 
137                 private void evaluateWithZeroButtons() throws Throwable {
138                     if (setActiveOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY,
139                             LauncherInstrumentation.NavigationModel.ZERO_BUTTON)) {
140                         base.evaluate();
141                     }
142                 }
143 
144                 private boolean packageExists(String packageName) {
145                     try {
146                         PackageManager pm = getInstrumentation().getContext().getPackageManager();
147                         if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) {
148                             return false;
149                         }
150                     } catch (PackageManager.NameNotFoundException e) {
151                         return false;
152                     }
153                     return true;
154                 }
155 
156                 private boolean setActiveOverlay(String overlayPackage,
157                         LauncherInstrumentation.NavigationModel expectedMode) throws Exception {
158                     if (!packageExists(overlayPackage)) {
159                         Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
160                         return false;
161                     }
162 
163                     setOverlayPackageEnabled(NAV_BAR_MODE_3BUTTON_OVERLAY,
164                             overlayPackage == NAV_BAR_MODE_3BUTTON_OVERLAY);
165                     setOverlayPackageEnabled(NAV_BAR_MODE_2BUTTON_OVERLAY,
166                             overlayPackage == NAV_BAR_MODE_2BUTTON_OVERLAY);
167                     setOverlayPackageEnabled(NAV_BAR_MODE_GESTURAL_OVERLAY,
168                             overlayPackage == NAV_BAR_MODE_GESTURAL_OVERLAY);
169 
170                     if (currentSysUiNavigationMode() != expectedMode) {
171                         final CountDownLatch latch = new CountDownLatch(1);
172                         final Context targetContext = getInstrumentation().getTargetContext();
173                         final SysUINavigationMode.NavigationModeChangeListener listener =
174                                 newMode -> {
175                                     if (LauncherInstrumentation.getNavigationModel(newMode.resValue)
176                                             == expectedMode) {
177                                         latch.countDown();
178                                     }
179                                 };
180                         final SysUINavigationMode sysUINavigationMode =
181                                 SysUINavigationMode.INSTANCE.get(targetContext);
182                         targetContext.getMainExecutor().execute(() ->
183                                 sysUINavigationMode.addModeChangeListener(listener));
184                         latch.await(60, TimeUnit.SECONDS);
185                         targetContext.getMainExecutor().execute(() ->
186                                 sysUINavigationMode.removeModeChangeListener(listener));
187                         assertTrue("Navigation mode didn't change to " + expectedMode,
188                                 currentSysUiNavigationMode() == expectedMode);
189                     }
190 
191                     for (int i = 0; i != 100; ++i) {
192                         if (mLauncher.getNavigationModel() == expectedMode) break;
193                         Thread.sleep(100);
194                     }
195                     assertTrue("Couldn't switch to " + overlayPackage,
196                             mLauncher.getNavigationModel() == expectedMode);
197 
198                     for (int i = 0; i != 100; ++i) {
199                         if (mLauncher.getNavigationModeMismatchError() == null) break;
200                         Thread.sleep(100);
201                     }
202                     final String error = mLauncher.getNavigationModeMismatchError();
203                     assertTrue("Switching nav mode: " + error, error == null);
204 
205                     Thread.sleep(5000);
206                     return true;
207                 }
208 
209                 private void setOverlayPackageEnabled(String overlayPackage, boolean enable)
210                         throws Exception {
211                     Log.d(TAG, "setOverlayPackageEnabled: " + overlayPackage + " " + enable);
212                     final String action = enable ? "enable" : "disable";
213                     UiDevice.getInstance(getInstrumentation()).executeShellCommand(
214                             "cmd overlay " + action + " " + overlayPackage);
215                 }
216             };
217         } else {
218             return base;
219         }
220     }
221 
222     private static LauncherInstrumentation.NavigationModel currentSysUiNavigationMode() {
223         return LauncherInstrumentation.getNavigationModel(
224                 SysUINavigationMode.getMode(
225                         getInstrumentation().
226                                 getTargetContext()).
227                         resValue);
228     }
229 }
230