• 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 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