• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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