1 /* 2 * Copyright (C) 2017 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 android.view.inputmethod.cts.util; 18 19 import static android.support.test.InstrumentationRegistry.getInstrumentation; 20 import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; 21 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 22 23 import android.app.AlertDialog; 24 import android.app.Instrumentation; 25 import android.content.Intent; 26 import android.graphics.Bitmap; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import androidx.annotation.ColorInt; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import android.support.test.InstrumentationRegistry; 33 import android.util.Size; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.WindowInsets; 37 import android.widget.TextView; 38 39 import java.util.concurrent.TimeUnit; 40 import java.util.concurrent.atomic.AtomicReference; 41 42 /** 43 * A utility class to write tests that depend on some capabilities related to navigation bar. 44 */ 45 public class NavigationBarInfo { 46 private static final long BEFORE_SCREENSHOT_WAIT = TimeUnit.SECONDS.toMillis(1); 47 48 private final boolean mHasBottomNavigationBar; 49 private final int mBottomNavigationBerHeight; 50 private final boolean mSupportsNavigationBarColor; 51 private final boolean mSupportsLightNavigationBar; 52 private final boolean mSupportsDimmingWindowLightNavigationBarOverride; 53 54 private NavigationBarInfo(boolean hasBottomNavigationBar, int bottomNavigationBerHeight, 55 boolean supportsNavigationBarColor, boolean supportsLightNavigationBar, 56 boolean supportsDimmingWindowLightNavigationBarOverride) { 57 mHasBottomNavigationBar = hasBottomNavigationBar; 58 mBottomNavigationBerHeight = bottomNavigationBerHeight; 59 mSupportsNavigationBarColor = supportsNavigationBarColor; 60 mSupportsLightNavigationBar = supportsLightNavigationBar; 61 mSupportsDimmingWindowLightNavigationBarOverride = 62 supportsDimmingWindowLightNavigationBarOverride; 63 } 64 65 @Nullable 66 private static NavigationBarInfo sInstance; 67 68 /** 69 * Returns a {@link NavigationBarInfo} instance. 70 * 71 * <p>As a performance optimizations, this method internally caches the previous result and 72 * returns the same result if this gets called multiple times.</p> 73 * 74 * <p>Note: The caller should be aware that this method may launch {@link TestActivity} 75 * internally.</p> 76 * 77 * @return {@link NavigationBarInfo} obtained with {@link TestActivity}. 78 */ 79 @NonNull 80 public static NavigationBarInfo getInstance() throws Exception { 81 if (sInstance != null) { 82 return sInstance; 83 } 84 85 final int actualBottomInset; 86 { 87 final AtomicReference<View> viewRef = new AtomicReference<>(); 88 TestActivity.startSync(activity -> { 89 final View view = new View(activity); 90 view.setLayoutParams(new ViewGroup.LayoutParams( 91 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 92 viewRef.set(view); 93 return view; 94 }, Intent.FLAG_ACTIVITY_NO_ANIMATION); 95 96 final View view = viewRef.get(); 97 98 final WindowInsets windowInsets = getOnMainSync(() -> view.getRootWindowInsets()); 99 if (!windowInsets.hasStableInsets() || windowInsets.getStableInsetBottom() <= 0) { 100 return new NavigationBarInfo(false, 0, false, false, false); 101 } 102 final Size displaySize = getOnMainSync(() -> { 103 final Point size = new Point(); 104 view.getDisplay().getRealSize(size); 105 return new Size(size.x, size.y); 106 }); 107 108 final Rect viewBoundsOnScreen = getOnMainSync(() -> { 109 final int[] xy = new int[2]; 110 view.getLocationOnScreen(xy); 111 final int x = xy[0]; 112 final int y = xy[1]; 113 return new Rect(x, y, x + view.getWidth(), y + view.getHeight()); 114 }); 115 actualBottomInset = displaySize.getHeight() - viewBoundsOnScreen.bottom; 116 if (actualBottomInset != windowInsets.getStableInsetBottom()) { 117 sInstance = new NavigationBarInfo(false, 0, false, false, false); 118 return sInstance; 119 } 120 } 121 122 final boolean colorSupported = NavigationBarColorVerifier.verify( 123 color -> getBottomNavigationBarBitmapForActivity( 124 color, false /* lightNavigationBar */, actualBottomInset, 125 false /* showDimmingDialog */)).getResult() 126 == NavigationBarColorVerifier.ResultType.SUPPORTED; 127 128 final boolean lightModeSupported = LightNavigationBarVerifier.verify( 129 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity( 130 color, lightNavigationBar, actualBottomInset, 131 false /* showDimmingDialog */)).getResult() 132 == LightNavigationBarVerifier.ResultType.SUPPORTED; 133 134 final boolean dimmingSupported = lightModeSupported && LightNavigationBarVerifier.verify( 135 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity( 136 color, lightNavigationBar, actualBottomInset, 137 true /* showDimmingDialog */)).getResult() 138 == LightNavigationBarVerifier.ResultType.NOT_SUPPORTED; 139 140 sInstance = new NavigationBarInfo( 141 true, actualBottomInset, colorSupported, lightModeSupported, dimmingSupported); 142 return sInstance; 143 } 144 145 @NonNull 146 private static Bitmap getBottomNavigationBarBitmapForActivity( 147 @ColorInt int navigationBarColor, boolean lightNavigationBar, 148 int bottomNavigationBarHeight, boolean showDimmingDialog) throws Exception { 149 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 150 151 152 final TestActivity testActivity = TestActivity.startSync(activity -> { 153 final View view = new View(activity); 154 activity.getWindow().setNavigationBarColor(navigationBarColor); 155 156 // Set/unset SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR if necessary. 157 final int currentVis = view.getSystemUiVisibility(); 158 final int newVis = (currentVis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) 159 | (lightNavigationBar ? View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0); 160 if (currentVis != newVis) { 161 view.setSystemUiVisibility(newVis); 162 } 163 164 view.setLayoutParams(new ViewGroup.LayoutParams( 165 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 166 return view; 167 }); 168 instrumentation.waitForIdleSync(); 169 170 final AlertDialog dialog; 171 if (showDimmingDialog) { 172 dialog = getOnMainSync(() -> { 173 final TextView textView = new TextView(testActivity); 174 textView.setText("Dimming Window"); 175 final AlertDialog alertDialog = new AlertDialog.Builder(testActivity) 176 .setView(textView) 177 .create(); 178 alertDialog.getWindow().setFlags(FLAG_DIM_BEHIND, FLAG_DIM_BEHIND); 179 alertDialog.show(); 180 return alertDialog; 181 }); 182 } else { 183 dialog = null; 184 } 185 186 Thread.sleep(BEFORE_SCREENSHOT_WAIT); 187 188 final Bitmap fullBitmap = getInstrumentation().getUiAutomation().takeScreenshot(); 189 final Bitmap bottomNavBarBitmap = Bitmap.createBitmap(fullBitmap, 0, 190 fullBitmap.getHeight() - bottomNavigationBarHeight, fullBitmap.getWidth(), 191 bottomNavigationBarHeight); 192 if (dialog != null) { 193 // Dialog#dismiss() is a thread safe method so we don't need to call this from the UI 194 // thread. 195 dialog.dismiss(); 196 } 197 return bottomNavBarBitmap; 198 } 199 200 /** 201 * @return {@code true} if this device seems to have bottom navigation bar. 202 */ 203 public boolean hasBottomNavigationBar() { 204 return mHasBottomNavigationBar; 205 } 206 207 /** 208 * @return height of the bottom navigation bar. Valid only when 209 * {@link #hasBottomNavigationBar()} returns {@code true} 210 */ 211 public int getBottomNavigationBerHeight() { 212 return mBottomNavigationBerHeight; 213 } 214 215 /** 216 * @return {@code true} if {@link android.view.Window#setNavigationBarColor(int)} seem to take 217 * effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns 218 * {@code true} 219 */ 220 public boolean supportsNavigationBarColor() { 221 return mSupportsNavigationBarColor; 222 } 223 224 /** 225 * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seem to 226 * take effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns 227 * {@code true} 228 */ 229 public boolean supportsLightNavigationBar() { 230 return mSupportsLightNavigationBar; 231 } 232 233 /** 234 * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} will be 235 * canceled when a {@link android.view.Window} with 236 * {@link android.view.WindowManager.LayoutParams#FLAG_DIM_BEHIND} is shown. 237 */ 238 public boolean supportsDimmingWindowLightNavigationBarOverride() { 239 return mSupportsDimmingWindowLightNavigationBarOverride; 240 } 241 } 242