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.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; 20 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 21 22 import android.app.AlertDialog; 23 import android.app.Instrumentation; 24 import android.content.Intent; 25 import android.graphics.Bitmap; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.util.Size; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.WindowInsets; 32 import android.widget.TextView; 33 34 import androidx.annotation.ColorInt; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.test.platform.app.InstrumentationRegistry; 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 NavigationBarInfo(boolean hasBottomNavigationBar, int bottomNavigationBerHeight, boolean supportsNavigationBarColor, boolean supportsLightNavigationBar, boolean supportsDimmingWindowLightNavigationBarOverride)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 getInstance()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 new TestActivity.Starter() 89 .withAdditionalFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) 90 .startSync(activity -> { 91 final View view = new View(activity); 92 view.setLayoutParams(new ViewGroup.LayoutParams( 93 ViewGroup.LayoutParams.MATCH_PARENT, 94 ViewGroup.LayoutParams.MATCH_PARENT)); 95 viewRef.set(view); 96 return view; 97 }, TestActivity.class); 98 99 final View view = viewRef.get(); 100 101 final WindowInsets windowInsets = getOnMainSync(() -> view.getRootWindowInsets()); 102 if (!windowInsets.hasStableInsets() || windowInsets.getStableInsetBottom() <= 0) { 103 return new NavigationBarInfo(false, 0, false, false, false); 104 } 105 final Size displaySize = getOnMainSync(() -> { 106 final Point size = new Point(); 107 view.getDisplay().getRealSize(size); 108 return new Size(size.x, size.y); 109 }); 110 111 final Rect viewBoundsOnScreen = getOnMainSync(() -> { 112 final int[] xy = new int[2]; 113 view.getLocationOnScreen(xy); 114 final int x = xy[0]; 115 final int y = xy[1]; 116 return new Rect(x, y, x + view.getWidth(), y + view.getHeight()); 117 }); 118 actualBottomInset = displaySize.getHeight() - viewBoundsOnScreen.bottom; 119 if (actualBottomInset != windowInsets.getStableInsetBottom()) { 120 sInstance = new NavigationBarInfo(false, 0, false, false, false); 121 return sInstance; 122 } 123 } 124 125 final boolean colorSupported = NavigationBarColorVerifier.verify( 126 color -> getBottomNavigationBarBitmapForActivity( 127 color, false /* lightNavigationBar */, actualBottomInset, 128 false /* showDimmingDialog */)).getResult() 129 == NavigationBarColorVerifier.ResultType.SUPPORTED; 130 131 final boolean lightModeSupported = LightNavigationBarVerifier.verify( 132 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity( 133 color, lightNavigationBar, actualBottomInset, 134 false /* showDimmingDialog */)).getResult() 135 == LightNavigationBarVerifier.ResultType.SUPPORTED; 136 137 final boolean dimmingSupported = lightModeSupported && LightNavigationBarVerifier.verify( 138 (color, lightNavigationBar) -> getBottomNavigationBarBitmapForActivity( 139 color, lightNavigationBar, actualBottomInset, 140 true /* showDimmingDialog */)).getResult() 141 == LightNavigationBarVerifier.ResultType.SUPPORTED; 142 143 sInstance = new NavigationBarInfo( 144 true, actualBottomInset, colorSupported, lightModeSupported, dimmingSupported); 145 return sInstance; 146 } 147 148 @NonNull getBottomNavigationBarBitmapForActivity( @olorInt int navigationBarColor, boolean lightNavigationBar, int bottomNavigationBarHeight, boolean showDimmingDialog)149 private static Bitmap getBottomNavigationBarBitmapForActivity( 150 @ColorInt int navigationBarColor, boolean lightNavigationBar, 151 int bottomNavigationBarHeight, boolean showDimmingDialog) throws Exception { 152 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 153 154 155 final TestActivity testActivity = TestActivity.startSync(activity -> { 156 final View view = new View(activity); 157 activity.getWindow().setNavigationBarColor(navigationBarColor); 158 159 // Set/unset SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR if necessary. 160 final int currentVis = view.getSystemUiVisibility(); 161 final int newVis = (currentVis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) 162 | (lightNavigationBar ? View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0); 163 if (currentVis != newVis) { 164 view.setSystemUiVisibility(newVis); 165 } 166 167 view.setLayoutParams(new ViewGroup.LayoutParams( 168 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 169 return view; 170 }); 171 instrumentation.waitForIdleSync(); 172 173 final AlertDialog dialog; 174 if (showDimmingDialog) { 175 dialog = getOnMainSync(() -> { 176 final TextView textView = new TextView(testActivity); 177 textView.setText("Dimming Window"); 178 final AlertDialog alertDialog = new AlertDialog.Builder(testActivity) 179 .setView(textView) 180 .create(); 181 alertDialog.getWindow().setFlags(FLAG_DIM_BEHIND, FLAG_DIM_BEHIND); 182 alertDialog.show(); 183 return alertDialog; 184 }); 185 } else { 186 dialog = null; 187 } 188 189 Thread.sleep(BEFORE_SCREENSHOT_WAIT); 190 191 final Bitmap fullBitmap = instrumentation.getUiAutomation().takeScreenshot(); 192 final Bitmap bottomNavBarBitmap = Bitmap.createBitmap(fullBitmap, 0, 193 fullBitmap.getHeight() - bottomNavigationBarHeight, fullBitmap.getWidth(), 194 bottomNavigationBarHeight); 195 if (dialog != null) { 196 // Dialog#dismiss() is a thread safe method so we don't need to call this from the UI 197 // thread. 198 dialog.dismiss(); 199 } 200 return bottomNavBarBitmap; 201 } 202 203 /** 204 * @return {@code true} if this device seems to have bottom navigation bar. 205 */ hasBottomNavigationBar()206 public boolean hasBottomNavigationBar() { 207 return mHasBottomNavigationBar; 208 } 209 210 /** 211 * @return height of the bottom navigation bar. Valid only when 212 * {@link #hasBottomNavigationBar()} returns {@code true} 213 */ getBottomNavigationBerHeight()214 public int getBottomNavigationBerHeight() { 215 return mBottomNavigationBerHeight; 216 } 217 218 /** 219 * @return {@code true} if {@link android.view.Window#setNavigationBarColor(int)} seem to take 220 * effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns 221 * {@code true} 222 */ supportsNavigationBarColor()223 public boolean supportsNavigationBarColor() { 224 return mSupportsNavigationBarColor; 225 } 226 227 /** 228 * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} seem to 229 * take effect on this device. Valid only when {@link #hasBottomNavigationBar()} returns 230 * {@code true} 231 */ supportsLightNavigationBar()232 public boolean supportsLightNavigationBar() { 233 return mSupportsLightNavigationBar; 234 } 235 236 /** 237 * @return {@code true} if {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR} will be 238 * canceled when a {@link android.view.Window} with 239 * {@link android.view.WindowManager.LayoutParams#FLAG_DIM_BEHIND} is shown. 240 */ supportsDimmingWindowLightNavigationBarOverride()241 public boolean supportsDimmingWindowLightNavigationBarOverride() { 242 return mSupportsDimmingWindowLightNavigationBarOverride; 243 } 244 } 245