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 org.junit.Assume.assumeTrue; 20 21 import android.app.Instrumentation; 22 import android.content.Context; 23 import android.content.pm.PackageManager; 24 import android.content.res.Resources; 25 import android.graphics.Insets; 26 import android.graphics.Rect; 27 import android.os.SystemClock; 28 import android.support.test.uiautomator.UiDevice; 29 import android.view.WindowInsets; 30 import android.view.WindowManager; 31 32 import androidx.test.InstrumentationRegistry; 33 34 import org.junit.ClassRule; 35 import org.junit.rules.ExternalResource; 36 37 import java.io.IOException; 38 39 /** 40 * Test rule to enable gesture navigation on the device. Designed to be a {@link ClassRule}. 41 */ 42 public class GestureNavRule extends ExternalResource { 43 private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode"; 44 private static final int NAV_BAR_MODE_3BUTTON = 0; 45 private static final int NAV_BAR_MODE_2BUTTON = 1; 46 private static final int NAV_BAR_MODE_GESTURAL = 2; 47 48 private static final String NAV_BAR_MODE_3BUTTON_OVERLAY = 49 "com.android.internal.systemui.navbar.threebutton"; 50 private static final String NAV_BAR_MODE_2BUTTON_OVERLAY = 51 "com.android.internal.systemui.navbar.twobutton"; 52 private static final String GESTURAL_OVERLAY_NAME = 53 "com.android.internal.systemui.navbar.gestural"; 54 55 private static final int WAIT_OVERLAY_TIMEOUT = 3000; 56 private static final int PEEK_INTERVAL = 200; 57 58 private final Context mTargetContext; 59 private final UiDevice mDevice; 60 private final WindowManager mWindowManager; 61 62 private final String mOriginalOverlayPackage; 63 64 @Override before()65 protected void before() throws Throwable { 66 if (!isGestureMode()) { 67 enableGestureNav(); 68 } 69 } 70 71 @Override after()72 protected void after() { 73 if (!mOriginalOverlayPackage.equals(GESTURAL_OVERLAY_NAME)) { 74 disableGestureNav(); 75 } 76 } 77 78 /** 79 * Initialize all options in System Gesture. 80 */ GestureNavRule()81 public GestureNavRule() { 82 @SuppressWarnings("deprecation") 83 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 84 mDevice = UiDevice.getInstance(instrumentation); 85 mTargetContext = instrumentation.getTargetContext(); 86 87 mOriginalOverlayPackage = getCurrentOverlayPackage(); 88 mWindowManager = mTargetContext.getSystemService(WindowManager.class); 89 } 90 91 @SuppressWarnings("BooleanMethodIsAlwaysInverted") hasSystemGestureFeature()92 private boolean hasSystemGestureFeature() { 93 if (!containsNavigationBar()) { 94 return false; 95 } 96 final PackageManager pm = mTargetContext.getPackageManager(); 97 98 // No bars on embedded devices. 99 // No bars on TVs and watches. 100 return !(pm.hasSystemFeature(PackageManager.FEATURE_WATCH) 101 || pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED) 102 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 103 || pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)); 104 } 105 getCurrentOverlayPackage()106 private String getCurrentOverlayPackage() { 107 final int currentNavMode = getCurrentNavMode(); 108 switch (currentNavMode) { 109 case NAV_BAR_MODE_GESTURAL: 110 return GESTURAL_OVERLAY_NAME; 111 case NAV_BAR_MODE_2BUTTON: 112 return NAV_BAR_MODE_2BUTTON_OVERLAY; 113 case NAV_BAR_MODE_3BUTTON: 114 default: 115 return NAV_BAR_MODE_3BUTTON_OVERLAY; 116 } 117 } 118 insetsToRect(Insets insets, Rect outRect)119 private void insetsToRect(Insets insets, Rect outRect) { 120 outRect.set(insets.left, insets.top, insets.right, insets.bottom); 121 } 122 enableGestureNav()123 private void enableGestureNav() { 124 if (!hasSystemGestureFeature()) { 125 return; 126 } 127 try { 128 if (!mDevice.executeShellCommand("cmd overlay list").contains(GESTURAL_OVERLAY_NAME)) { 129 return; 130 } 131 } catch (IOException ignore) { 132 // 133 } 134 monitorOverlayChange(() -> { 135 try { 136 mDevice.executeShellCommand("cmd overlay enable " + GESTURAL_OVERLAY_NAME); 137 } catch (IOException e) { 138 // Do nothing 139 } 140 }); 141 } 142 disableGestureNav()143 private void disableGestureNav() { 144 if (!hasSystemGestureFeature()) { 145 return; 146 } 147 monitorOverlayChange(() -> { 148 try { 149 mDevice.executeShellCommand("cmd overlay enable " + mOriginalOverlayPackage); 150 } catch (IOException ignore) { 151 // Do nothing 152 } 153 }); 154 } 155 getCurrentInsetsSize(Rect outSize)156 private void getCurrentInsetsSize(Rect outSize) { 157 outSize.setEmpty(); 158 if (mWindowManager != null) { 159 WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); 160 Insets navInsets = insets.getInsetsIgnoringVisibility( 161 WindowInsets.Type.navigationBars()); 162 insetsToRect(navInsets, outSize); 163 } 164 } 165 166 // Monitoring the navigation bar insets size change as a hint of gesture mode has changed, not 167 // the best option for every kind of devices. We can consider listening OVERLAY_CHANGED 168 // broadcast in U. monitorOverlayChange(Runnable overlayChangeCommand)169 private void monitorOverlayChange(Runnable overlayChangeCommand) { 170 if (mWindowManager != null) { 171 final Rect initSize = new Rect(); 172 getCurrentInsetsSize(initSize); 173 174 overlayChangeCommand.run(); 175 // wait for insets size change 176 final Rect peekSize = new Rect(); 177 int t = 0; 178 while (t < WAIT_OVERLAY_TIMEOUT) { 179 SystemClock.sleep(PEEK_INTERVAL); 180 t += PEEK_INTERVAL; 181 getCurrentInsetsSize(peekSize); 182 if (!peekSize.equals(initSize)) { 183 break; 184 } 185 } 186 } else { 187 // shouldn't happen 188 overlayChangeCommand.run(); 189 SystemClock.sleep(WAIT_OVERLAY_TIMEOUT); 190 } 191 } 192 193 /** 194 * Assumes the device is in gesture navigation mode. Due to constraints of AndroidJUnitRunner we 195 * can't make assumptions in static contexts like in a {@link ClassRule} so tests need to call 196 * this method explicitly. 197 */ assumeGestureNavigationMode()198 public void assumeGestureNavigationMode() { 199 boolean isGestureMode = isGestureMode(); 200 assumeTrue("Gesture navigation required", isGestureMode); 201 } 202 getCurrentNavMode()203 private int getCurrentNavMode() { 204 Resources res = mTargetContext.getResources(); 205 int naviModeId = res.getIdentifier(NAV_BAR_INTERACTION_MODE_RES_NAME, "integer", "android"); 206 return res.getInteger(naviModeId); 207 } 208 containsNavigationBar()209 private boolean containsNavigationBar() { 210 final Rect peekSize = new Rect(); 211 getCurrentInsetsSize(peekSize); 212 return peekSize.height() != 0; 213 } 214 isGestureMode()215 private boolean isGestureMode() { 216 if (!containsNavigationBar()) { 217 return false; 218 } 219 final int naviMode = getCurrentNavMode(); 220 return naviMode == NAV_BAR_MODE_GESTURAL; 221 } 222 } 223