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