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.platform.app.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.ZERO_BUTTON; 24 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY; 25 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY; 26 27 import android.content.Context; 28 import android.content.pm.PackageManager; 29 import android.os.Process; 30 import android.util.Log; 31 32 import androidx.test.uiautomator.UiDevice; 33 34 import com.android.launcher3.tapl.LauncherInstrumentation; 35 import com.android.launcher3.tapl.TestHelpers; 36 import com.android.launcher3.ui.AbstractLauncherUiTest; 37 import com.android.launcher3.util.DisplayController; 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 @NavigationModeSwitch. 56 */ 57 public class NavigationModeSwitchRule implements TestRule { 58 59 static final String TAG = "QuickStepOnOffRule"; 60 61 public enum Mode { 62 THREE_BUTTON, ZERO_BUTTON, ALL 63 } 64 65 // Annotation for tests that need to be run with quickstep enabled and disabled. 66 @Retention(RetentionPolicy.RUNTIME) 67 @Target(ElementType.METHOD) 68 public @interface NavigationModeSwitch { mode()69 Mode mode() default ALL; 70 } 71 72 private final LauncherInstrumentation mLauncher; 73 74 static final DisplayController DISPLAY_CONTROLLER = 75 DisplayController.INSTANCE.get(getInstrumentation().getTargetContext()); 76 NavigationModeSwitchRule(LauncherInstrumentation launcher)77 public NavigationModeSwitchRule(LauncherInstrumentation launcher) { 78 mLauncher = launcher; 79 } 80 81 @Override apply(Statement base, Description description)82 public Statement apply(Statement base, Description description) { 83 if (TestHelpers.isInLauncherProcess() && 84 description.getAnnotation(NavigationModeSwitch.class) != null) { 85 Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode(); 86 return new Statement() { 87 @Override 88 public void evaluate() throws Throwable { 89 mLauncher.enableDebugTracing(); 90 final Context context = getInstrumentation().getContext(); 91 final int currentInteractionMode = 92 LauncherInstrumentation.getCurrentInteractionMode(context); 93 final String prevOverlayPkg = getCurrentOverlayPackage(currentInteractionMode); 94 final LauncherInstrumentation.NavigationModel originalMode = 95 mLauncher.getNavigationModel(); 96 try { 97 if (mode == ZERO_BUTTON || mode == ALL) { 98 evaluateWithZeroButtons(); 99 } 100 if (mode == THREE_BUTTON || mode == ALL) { 101 evaluateWithThreeButtons(); 102 } 103 } catch (Throwable e) { 104 Log.e(TAG, "Error", e); 105 throw e; 106 } finally { 107 Log.d(TAG, "In Finally block"); 108 assertTrue(mLauncher, "Couldn't set overlay", 109 setActiveOverlay(mLauncher, prevOverlayPkg, originalMode, 110 description), description); 111 } 112 } 113 114 private void evaluateWithThreeButtons() throws Throwable { 115 if (setActiveOverlay(mLauncher, NAV_BAR_MODE_3BUTTON_OVERLAY, 116 LauncherInstrumentation.NavigationModel.THREE_BUTTON, description)) { 117 base.evaluate(); 118 } 119 } 120 121 private void evaluateWithZeroButtons() throws Throwable { 122 if (setActiveOverlay(mLauncher, NAV_BAR_MODE_GESTURAL_OVERLAY, 123 LauncherInstrumentation.NavigationModel.ZERO_BUTTON, description)) { 124 base.evaluate(); 125 } 126 } 127 }; 128 } else { 129 return base; 130 } 131 } 132 133 public static String getCurrentOverlayPackage(int currentInteractionMode) { 134 return QuickStepContract.isGesturalMode(currentInteractionMode) 135 ? NAV_BAR_MODE_GESTURAL_OVERLAY 136 : NAV_BAR_MODE_3BUTTON_OVERLAY; 137 } 138 139 private static LauncherInstrumentation.NavigationModel currentSysUiNavigationMode() { 140 return LauncherInstrumentation.getNavigationModel( 141 DisplayController.getNavigationMode( 142 getInstrumentation(). 143 getTargetContext()). 144 resValue); 145 } 146 147 public static boolean setActiveOverlay(LauncherInstrumentation launcher, String overlayPackage, 148 LauncherInstrumentation.NavigationModel expectedMode, Description description) 149 throws Exception { 150 if (!packageExists(overlayPackage)) { 151 Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist"); 152 return false; 153 } 154 155 Log.d(TAG, "setActiveOverlay: " + overlayPackage + "..."); 156 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 157 String.format("cmd overlay enable-exclusive --user %d --category %s", 158 Process.myUserHandle().getIdentifier(), 159 overlayPackage)); 160 161 if (currentSysUiNavigationMode() != expectedMode) { 162 final CountDownLatch latch = new CountDownLatch(1); 163 final Context targetContext = getInstrumentation().getTargetContext(); 164 final DisplayController.DisplayInfoChangeListener listener = 165 (context, info, flags) -> { 166 if (LauncherInstrumentation.getNavigationModel( 167 info.getNavigationMode().resValue) == expectedMode) { 168 latch.countDown(); 169 } 170 }; 171 targetContext.getMainExecutor().execute(() -> 172 DISPLAY_CONTROLLER.addChangeListener(listener)); 173 latch.await(60, TimeUnit.SECONDS); 174 targetContext.getMainExecutor().execute(() -> 175 DISPLAY_CONTROLLER.removeChangeListener(listener)); 176 177 assertTrue(launcher, "Navigation mode didn't change to " + expectedMode, 178 currentSysUiNavigationMode() == expectedMode, description); 179 180 } 181 182 Wait.atMost("Couldn't switch to " + overlayPackage, 183 () -> launcher.getNavigationModel() == expectedMode, 184 launcher); 185 186 Wait.atMost(() -> "Switching nav mode: " 187 + launcher.getNavigationModeMismatchError(false), 188 () -> launcher.getNavigationModeMismatchError(false) == null, 189 launcher); 190 AbstractLauncherUiTest.checkDetectedLeaks(launcher, false); 191 return true; 192 } 193 194 private static boolean packageExists(String packageName) { 195 try { 196 PackageManager pm = getInstrumentation().getContext().getPackageManager(); 197 if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) { 198 return false; 199 } 200 } catch (PackageManager.NameNotFoundException e) { 201 return false; 202 } 203 return true; 204 } 205 206 private static void assertTrue(LauncherInstrumentation launcher, String message, 207 boolean condition, Description description) { 208 launcher.checkForAnomaly(true, true); 209 if (!condition) { 210 if (description != null) { 211 FailureWatcher.onError(launcher, description); 212 } 213 throw new AssertionError(message); 214 } 215 } 216 } 217