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 android.app.Instrumentation;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.content.res.Resources;
23 import android.graphics.Insets;
24 import android.graphics.Rect;
25 import android.os.SystemClock;
26 import android.view.WindowInsets;
27 import android.view.WindowManager;
28 
29 import androidx.test.InstrumentationRegistry;
30 import androidx.test.uiautomator.UiDevice;
31 
32 import java.io.IOException;
33 
34 /**
35  * Helper class to enable gesture navigation on the device.
36  */
37 public class GestureNavSwitchHelper {
38     private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode";
39     private static final int NAV_BAR_MODE_GESTURAL = 2;
40 
41     private static final String GESTURAL_OVERLAY_NAME =
42             "com.android.internal.systemui.navbar.gestural";
43 
44     private static final int WAIT_OVERLAY_TIMEOUT = 3000;
45     private static final int PEEK_INTERVAL = 200;
46 
47     private final Instrumentation mInstrumentation;
48     private final UiDevice mDevice;
49     private final WindowManager mWindowManager;
50     // This object has tried to enable gesture navigation but failed.
51     private boolean mTriedEnableButFail;
52 
53     /**
54      * Initialize all options in System Gesture.
55      */
GestureNavSwitchHelper()56     public GestureNavSwitchHelper() {
57         mInstrumentation = InstrumentationRegistry.getInstrumentation();
58         mDevice = UiDevice.getInstance(mInstrumentation);
59         final Context context = mInstrumentation.getTargetContext();
60 
61         mWindowManager = context.getSystemService(WindowManager.class);
62     }
63 
hasSystemGestureFeature()64     private boolean hasSystemGestureFeature() {
65         if (!containsNavigationBar()) {
66             return false;
67         }
68         Context context = mInstrumentation.getTargetContext();
69         final PackageManager pm = context.getPackageManager();
70 
71         // No bars on embedded devices.
72         // No bars on TVs and watches.
73         return !(pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
74                 || pm.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)
75                 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
76                 || pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
77     }
78 
insetsToRect(Insets insets, Rect outRect)79     private void insetsToRect(Insets insets, Rect outRect) {
80         outRect.set(insets.left, insets.top, insets.right, insets.bottom);
81     }
82 
83     /**
84      * Attempt to enable gesture navigation mode.
85      * @return true if gesture navigation mode is enabled.
86      */
enableGestureNavigationMode()87     public boolean enableGestureNavigationMode() {
88         // skip retry
89         if (mTriedEnableButFail) {
90             return false;
91         }
92         if (!hasSystemGestureFeature()) {
93             return false;
94         }
95         if (isGestureMode()) {
96             return true;
97         }
98         enableGestureNav();
99         final boolean success = isGestureMode();
100         mTriedEnableButFail = !success;
101         return success;
102     }
103 
enableGestureNav()104     private void enableGestureNav() {
105         if (!hasSystemGestureFeature()) {
106             return;
107         }
108         try {
109             if (!mDevice.executeShellCommand("cmd overlay list").contains(GESTURAL_OVERLAY_NAME)) {
110                 return;
111             }
112         } catch (IOException ignore) {
113             //
114         }
115         monitorOverlayChange(() -> {
116             try {
117                 mDevice.executeShellCommand("cmd overlay enable " + GESTURAL_OVERLAY_NAME);
118             } catch (IOException e) {
119                 // Do nothing
120             }
121         });
122     }
123 
getCurrentInsetsSize(Rect outSize)124     private void getCurrentInsetsSize(Rect outSize) {
125         outSize.setEmpty();
126         if (mWindowManager != null) {
127             WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
128             Insets navInsets = insets.getInsetsIgnoringVisibility(
129                     WindowInsets.Type.navigationBars());
130             insetsToRect(navInsets, outSize);
131         }
132     }
133 
134     // Monitoring the navigation bar insets size change as a hint of gesture mode has changed, not
135     // the best option for every kind of devices. We can consider listening OVERLAY_CHANGED
136     // broadcast in U.
monitorOverlayChange(Runnable overlayChangeCommand)137     private void monitorOverlayChange(Runnable overlayChangeCommand) {
138         if (mWindowManager != null) {
139             final Rect initSize = new Rect();
140             getCurrentInsetsSize(initSize);
141             overlayChangeCommand.run();
142             // wait for insets size change
143             final Rect peekSize = new Rect();
144             int t = 0;
145             while (t < WAIT_OVERLAY_TIMEOUT) {
146                 SystemClock.sleep(PEEK_INTERVAL);
147                 t += PEEK_INTERVAL;
148                 getCurrentInsetsSize(peekSize);
149                 if (!peekSize.equals(initSize)) {
150                     break;
151                 }
152             }
153         } else {
154             // shouldn't happen
155             overlayChangeCommand.run();
156             SystemClock.sleep(WAIT_OVERLAY_TIMEOUT);
157         }
158     }
159 
getCurrentNavMode()160     private int getCurrentNavMode() {
161         final Context context  = mInstrumentation.getTargetContext();
162         final Resources res = context.getResources();
163         int naviModeId = res.getIdentifier(NAV_BAR_INTERACTION_MODE_RES_NAME, "integer", "android");
164         return res.getInteger(naviModeId);
165     }
166 
containsNavigationBar()167     private boolean containsNavigationBar() {
168         final Rect peekSize = new Rect();
169         getCurrentInsetsSize(peekSize);
170         return peekSize.height() != 0;
171     }
172 
173     /**
174      * @return Whether gesture navigation mode is enabled.
175      */
isGestureMode()176     public boolean isGestureMode() {
177         if (!containsNavigationBar()) {
178             return false;
179         }
180         final int naviMode = getCurrentNavMode();
181         return naviMode == NAV_BAR_MODE_GESTURAL;
182     }
183 }
184