1 /* 2 * Copyright (C) 2022 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.compatibility.common.util; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.graphics.Insets; 24 import android.graphics.Rect; 25 import android.os.Build; 26 import android.util.Log; 27 import android.view.WindowInsets; 28 import android.view.WindowManager; 29 30 import androidx.annotation.NonNull; 31 import androidx.test.platform.app.InstrumentationRegistry; 32 import androidx.test.uiautomator.UiDevice; 33 34 import java.io.IOException; 35 36 /** 37 * Helper class to enable gesture navigation on the device. 38 */ 39 public final class GestureNavSwitchHelper { 40 41 private static final String TAG = "GestureNavSwitchHelper"; 42 43 private static final String NAV_BAR_MODE_3BUTTON_OVERLAY = 44 "com.android.internal.systemui.navbar.threebutton"; 45 46 private static final String NAV_BAR_MODE_GESTURAL_OVERLAY = 47 "com.android.internal.systemui.navbar.gestural"; 48 49 private static final String STATE_ENABLED = "STATE_ENABLED"; 50 51 private static final String STATE_DISABLED = "STATE_DISABLED"; 52 53 private static final String STATE_UNKNOWN = "STATE_UNKNOWN"; 54 55 private static final long OVERLAY_WAIT_TIMEOUT = 10000; 56 57 private final UiDevice mDevice; 58 private final WindowManager mWindowManager; 59 /** Whether the device supports the navigation bar. */ 60 private final boolean mHasNavigationBar; 61 /** Failed to enable gesture navigation. */ 62 private boolean mEnableGestureNavFailed; 63 /** Failed to enable three button navigation. */ 64 private boolean mEnableThreeButtonNavFailed; 65 66 /** 67 * Initialize all options in System Gesture. 68 */ GestureNavSwitchHelper()69 public GestureNavSwitchHelper() { 70 final var instrumentation = InstrumentationRegistry.getInstrumentation(); 71 mDevice = UiDevice.getInstance(instrumentation); 72 final Context context = instrumentation.getTargetContext(); 73 74 mWindowManager = context.getSystemService(WindowManager.class); 75 76 final var pm = context.getPackageManager(); 77 // No bars on embedded devices. 78 // No bars on TVs and watches. 79 // No bars on PCs. 80 mHasNavigationBar = !(pm.hasSystemFeature(PackageManager.FEATURE_WATCH) 81 || pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED) 82 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 83 || pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) 84 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 85 && pm.hasSystemFeature(PackageManager.FEATURE_PC))); 86 } 87 88 /** Whether the device supports the navigation bar. */ hasNavigationBar()89 public boolean hasNavigationBar() { 90 return mHasNavigationBar && containsNavigationBar(); 91 } 92 insetsToRect(Insets insets, Rect outRect)93 private void insetsToRect(Insets insets, Rect outRect) { 94 outRect.set(insets.left, insets.top, insets.right, insets.bottom); 95 } 96 97 /** 98 * Checks whether the gesture navigation mode overlay is installed, regardless of whether it is 99 * enabled. 100 */ hasGestureNavOverlay()101 public boolean hasGestureNavOverlay() { 102 return hasOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY); 103 } 104 105 /** 106 * Checks whether the three button navigation mode overlay is installed, regardless of whether 107 * it is enabled. 108 */ hasThreeButtonNavOverlay()109 public boolean hasThreeButtonNavOverlay() { 110 return hasOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY); 111 } 112 113 /** Checks whether the given overlay is installed, regardless of whether it is enabled. */ hasOverlay(@onNull String overlayPackage)114 private boolean hasOverlay(@NonNull String overlayPackage) { 115 final String state = getStateForOverlay(overlayPackage); 116 return STATE_ENABLED.equals(state) || STATE_DISABLED.equals(state); 117 } 118 119 /** 120 * Enable gesture navigation mode. 121 * 122 * @return Whether the navigation mode was successfully set. This is {@code true} if the 123 * requested mode is already set. 124 */ enableGestureNavigationMode()125 public boolean enableGestureNavigationMode() { 126 // skip retry 127 if (mEnableGestureNavFailed) { 128 return false; 129 } 130 if (!hasNavigationBar()) { 131 return false; 132 } 133 if (isGestureMode()) { 134 return true; 135 } 136 final boolean success = setNavigationMode(NAV_BAR_MODE_GESTURAL_OVERLAY); 137 mEnableGestureNavFailed = !success; 138 return success; 139 } 140 141 /** 142 * Enable three button navigation mode. 143 * 144 * @return Whether the navigation mode was successfully set. This is {@code true} if the 145 * requested mode is already set. 146 */ enableThreeButtonNavigationMode()147 public boolean enableThreeButtonNavigationMode() { 148 // skip retry 149 if (mEnableThreeButtonNavFailed) { 150 return false; 151 } 152 if (!hasNavigationBar()) { 153 return true; 154 } 155 if (isThreeButtonMode()) { 156 return true; 157 } 158 final boolean success = setNavigationMode(NAV_BAR_MODE_3BUTTON_OVERLAY); 159 mEnableThreeButtonNavFailed = !success; 160 return success; 161 } 162 163 /** 164 * Sets the navigation mode to gesture navigation, if necessary. 165 * 166 * @return an {@link AutoCloseable} that resets the navigation mode, if necessary. 167 */ 168 @NonNull withGestureNavigationMode()169 public AutoCloseable withGestureNavigationMode() { 170 if (isGestureMode() || !hasNavigationBar()) { 171 return () -> {}; 172 } 173 174 assertWithMessage("Gesture navigation mode set") 175 .that(enableGestureNavigationMode()).isTrue(); 176 return () -> assertWithMessage("Gesture navigation mode unset") 177 .that(enableThreeButtonNavigationMode()).isTrue(); 178 } 179 180 /** 181 * Sets the navigation mode to three button navigation, if necessary. 182 * 183 * @return an {@link AutoCloseable} that resets the navigation mode, if necessary. 184 */ 185 @NonNull withThreeButtonNavigationMode()186 public AutoCloseable withThreeButtonNavigationMode() { 187 if (isThreeButtonMode() || !hasNavigationBar()) { 188 return () -> {}; 189 } 190 191 assertWithMessage("Three button navigation mode set") 192 .that(enableThreeButtonNavigationMode()).isTrue(); 193 return () -> assertWithMessage("Three button navigation mode unset") 194 .that(enableGestureNavigationMode()).isTrue(); 195 } 196 197 /** 198 * Sets the navigation mode exclusively (disabling all other modes). 199 * 200 * @param navigationModePkgName the package name of the navigation mode to be set. 201 * 202 * @return whether the navigation mode was successfully set. 203 */ setNavigationMode(@onNull String navigationModePkgName)204 private boolean setNavigationMode(@NonNull String navigationModePkgName) { 205 if (!hasOverlay(navigationModePkgName)) { 206 Log.i(TAG, "setNavigationMode, overlay: " + navigationModePkgName + " does not exist"); 207 return false; 208 } 209 Log.d(TAG, "setNavigationMode: " + navigationModePkgName); 210 try { 211 mDevice.executeShellCommand("cmd overlay enable-exclusive --category --user current " 212 + navigationModePkgName); 213 mDevice.executeShellCommand("am wait-for-broadcast-barrier"); 214 } catch (IOException e) { 215 Log.w(TAG, "Failed to set navigation mode", e); 216 return false; 217 } 218 return waitForOverlayState(navigationModePkgName, STATE_ENABLED); 219 } 220 getCurrentInsetsSize(@onNull Rect outSize)221 private void getCurrentInsetsSize(@NonNull Rect outSize) { 222 outSize.setEmpty(); 223 if (mWindowManager != null) { 224 WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); 225 Insets navInsets = insets.getInsetsIgnoringVisibility( 226 WindowInsets.Type.navigationBars()); 227 insetsToRect(navInsets, outSize); 228 } 229 } 230 containsNavigationBar()231 private boolean containsNavigationBar() { 232 final var peekSize = new Rect(); 233 getCurrentInsetsSize(peekSize); 234 return peekSize.height() != 0; 235 } 236 237 /** Whether three button navigation mode is enabled. */ isThreeButtonMode()238 public boolean isThreeButtonMode() { 239 return containsNavigationBar() 240 && STATE_ENABLED.equals(getStateForOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY)); 241 } 242 243 /** Whether gesture navigation mode is enabled. */ isGestureMode()244 public boolean isGestureMode() { 245 return containsNavigationBar() 246 && STATE_ENABLED.equals(getStateForOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY)); 247 } 248 249 /** 250 * Waits for the state of the overlay with the given package name to match the expected state. 251 * 252 * @param overlayPackage the package name of the overlay to check. 253 * @param expectedState the expected overlay state. 254 * 255 * @return whether the overlay state eventually matched the expected state. 256 */ waitForOverlayState(@onNull String overlayPackage, @NonNull String expectedState)257 private boolean waitForOverlayState(@NonNull String overlayPackage, 258 @NonNull String expectedState) { 259 final long startTime = System.currentTimeMillis(); 260 261 while (System.currentTimeMillis() - startTime < OVERLAY_WAIT_TIMEOUT) { 262 if (expectedState.equals(getStateForOverlay(overlayPackage))) { 263 return true; 264 } 265 } 266 267 Log.i(TAG, "waitForOverlayState, overlayPackage: " + overlayPackage + " state was not: " 268 + expectedState); 269 return false; 270 } 271 272 /** 273 * Returns the state of the overlay with the given package name. 274 * 275 * @param overlayPackage the package name of the overlay to check. 276 */ 277 @NonNull getStateForOverlay(@onNull String overlayPackage)278 private String getStateForOverlay(@NonNull String overlayPackage) { 279 try { 280 final String res = mDevice.executeShellCommand("cmd overlay dump --user current state " 281 + overlayPackage); 282 // When current user is not 0 (e.g. HSUM), the output will contain an additional line. 283 return res.split("\\r?\\n")[0].trim(); 284 } catch (IOException e) { 285 Log.w(TAG, "Failed to get overlay state", e); 286 return STATE_UNKNOWN; 287 } 288 } 289 } 290