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; 18 19 import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; 20 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 21 import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; 22 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 23 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 24 import static android.view.inputmethod.cts.util.LightNavigationBarVerifier.expectLightNavigationBarNotSupported; 25 import static android.view.inputmethod.cts.util.LightNavigationBarVerifier.expectLightNavigationBarSupported; 26 import static android.view.inputmethod.cts.util.NavigationBarColorVerifier.expectNavigationBarColorNotSupported; 27 import static android.view.inputmethod.cts.util.NavigationBarColorVerifier.expectNavigationBarColorSupported; 28 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync; 29 30 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; 31 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 32 import static com.android.cts.mockime.ImeEventStreamTestUtils.waitForInputViewLayoutStable; 33 34 import static org.junit.Assert.assertNotNull; 35 import static org.junit.Assume.assumeTrue; 36 37 import android.app.Activity; 38 import android.app.AlertDialog; 39 import android.app.UiAutomation; 40 import android.graphics.Bitmap; 41 import android.graphics.Color; 42 import android.os.Process; 43 import androidx.annotation.ColorInt; 44 import androidx.annotation.NonNull; 45 import android.support.test.InstrumentationRegistry; 46 import android.support.test.filters.MediumTest; 47 import android.support.test.runner.AndroidJUnit4; 48 import android.text.TextUtils; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.inputmethod.EditorInfo; 52 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 53 import android.view.inputmethod.cts.util.ImeAwareEditText; 54 import android.view.inputmethod.cts.util.NavigationBarInfo; 55 import android.view.inputmethod.cts.util.TestActivity; 56 import android.widget.LinearLayout; 57 import android.widget.TextView; 58 59 import com.android.cts.mockime.ImeEventStream; 60 import com.android.cts.mockime.ImeLayoutInfo; 61 import com.android.cts.mockime.ImeSettings; 62 import com.android.cts.mockime.MockImeSession; 63 64 import org.junit.BeforeClass; 65 import org.junit.Before; 66 import org.junit.Test; 67 import org.junit.runner.RunWith; 68 69 import java.util.concurrent.TimeUnit; 70 71 @MediumTest 72 @RunWith(AndroidJUnit4.class) 73 public class NavigationBarColorTest extends EndToEndImeTestBase { 74 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); 75 private static final long LAYOUT_STABLE_THRESHOLD = TimeUnit.SECONDS.toMillis(3); 76 77 private static final String TEST_MARKER = "android.view.inputmethod.cts.NavigationBarColorTest"; 78 79 private static void updateSystemUiVisibility(@NonNull View view, int flags, int mask) { 80 final int currentFlags = view.getSystemUiVisibility(); 81 final int newFlags = (currentFlags & ~mask) | (flags & mask); 82 if (currentFlags != newFlags) { 83 view.setSystemUiVisibility(newFlags); 84 } 85 } 86 87 @BeforeClass 88 public static void initializeNavigationBarInfo() throws Exception { 89 // Make sure that NavigationBarInfo is initialized before 90 // EndToEndImeTestBase#showStateInitializeActivity(). 91 NavigationBarInfo.getInstance(); 92 } 93 94 // TODO(b/37502066): Merge this back to initializeNavigationBarInfo() once b/37502066 is fixed. 95 @Before 96 public void checkNavigationBar() throws Exception { 97 assumeTrue("This test does not make sense if there is no navigation bar", 98 NavigationBarInfo.getInstance().hasBottomNavigationBar()); 99 100 assumeTrue("This test does not make sense if custom navigation bar color is not supported" 101 + " even for typical Activity", 102 NavigationBarInfo.getInstance().supportsNavigationBarColor()); 103 } 104 105 /** 106 * Represents test scenarios regarding how a {@link android.view.Window} that has 107 * {@link android.view.WindowManager.LayoutParams#FLAG_DIM_BEHIND} interacts with a different 108 * {@link android.view.Window} that has 109 * {@link android.view.View#SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR}. 110 */ 111 private enum DimmingTestMode { 112 /** 113 * No {@link AlertDialog} is shown when testing. 114 */ 115 NO_DIMMING_DIALOG, 116 /** 117 * An {@link AlertDialog} that has dimming effect is shown above the IME window. 118 */ 119 DIMMING_DIALOG_ABOVE_IME, 120 /** 121 * An {@link AlertDialog} that has dimming effect is shown behind the IME window. 122 */ 123 DIMMING_DIALOG_BEHIND_IME, 124 } 125 126 @NonNull 127 public TestActivity launchTestActivity(@ColorInt int navigationBarColor, 128 boolean lightNavigationBar, @NonNull DimmingTestMode dimmingTestMode) { 129 return TestActivity.startSync(activity -> { 130 final View contentView; 131 switch (dimmingTestMode) { 132 case NO_DIMMING_DIALOG: 133 case DIMMING_DIALOG_ABOVE_IME: { 134 final LinearLayout layout = new LinearLayout(activity); 135 layout.setOrientation(LinearLayout.VERTICAL); 136 final ImeAwareEditText editText = new ImeAwareEditText(activity); 137 editText.setPrivateImeOptions(TEST_MARKER); 138 editText.setHint("editText"); 139 editText.requestFocus(); 140 editText.scheduleShowSoftInput(); 141 layout.addView(editText); 142 contentView = layout; 143 break; 144 } 145 case DIMMING_DIALOG_BEHIND_IME: { 146 final View view = new View(activity); 147 view.setLayoutParams(new ViewGroup.LayoutParams( 148 ViewGroup.LayoutParams.MATCH_PARENT, 149 ViewGroup.LayoutParams.MATCH_PARENT)); 150 contentView = view; 151 break; 152 } 153 default: 154 throw new IllegalStateException("unknown mode=" + dimmingTestMode); 155 } 156 activity.getWindow().setNavigationBarColor(navigationBarColor); 157 updateSystemUiVisibility(contentView, 158 lightNavigationBar ? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR : 0, 159 SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 160 return contentView; 161 }); 162 } 163 164 private AutoCloseable showDialogIfNecessary( 165 @NonNull Activity activity, @NonNull DimmingTestMode dimmingTestMode) { 166 switch (dimmingTestMode) { 167 case NO_DIMMING_DIALOG: 168 // Dialog is not necessary. 169 return () -> { }; 170 case DIMMING_DIALOG_ABOVE_IME: { 171 final AlertDialog alertDialog = getOnMainSync(() -> { 172 final TextView textView = new TextView(activity); 173 textView.setText("Dummy"); 174 textView.requestFocus(); 175 final AlertDialog dialog = new AlertDialog.Builder(activity) 176 .setView(textView) 177 .create(); 178 dialog.getWindow().setFlags(FLAG_DIM_BEHIND | FLAG_ALT_FOCUSABLE_IM, 179 FLAG_DIM_BEHIND | FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM); 180 dialog.show(); 181 return dialog; 182 }); 183 // Note: Dialog#dismiss() is a thread safe method so we don't need to call this from 184 // the UI thread. 185 return () -> alertDialog.dismiss(); 186 } 187 case DIMMING_DIALOG_BEHIND_IME: { 188 final AlertDialog alertDialog = getOnMainSync(() -> { 189 final ImeAwareEditText editText = new ImeAwareEditText(activity); 190 editText.setPrivateImeOptions(TEST_MARKER); 191 editText.setHint("editText"); 192 editText.requestFocus(); 193 editText.scheduleShowSoftInput(); 194 final AlertDialog dialog = new AlertDialog.Builder(activity) 195 .setView(editText) 196 .create(); 197 dialog.getWindow().setFlags(FLAG_DIM_BEHIND, 198 FLAG_DIM_BEHIND | FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM); 199 dialog.show(); 200 return dialog; 201 }); 202 // Note: Dialog#dismiss() is a thread safe method so we don't need to call this from 203 // the UI thread. 204 return () -> alertDialog.dismiss(); 205 } 206 default: 207 throw new IllegalStateException("unknown mode=" + dimmingTestMode); 208 } 209 } 210 211 @NonNull 212 private ImeSettings.Builder imeSettingForSolidNavigationBar(@ColorInt int navigationBarColor, 213 boolean lightNavigationBar) { 214 final ImeSettings.Builder builder = new ImeSettings.Builder(); 215 builder.setNavigationBarColor(navigationBarColor); 216 if (lightNavigationBar) { 217 builder.setInputViewSystemUiVisibility(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 218 } 219 return builder; 220 } 221 222 @NonNull 223 private ImeSettings.Builder imeSettingForFloatingIme(@ColorInt int navigationBarColor, 224 boolean lightNavigationBar) { 225 final ImeSettings.Builder builder = new ImeSettings.Builder(); 226 builder.setWindowFlags(0, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 227 // As documented, Window#setNavigationBarColor() is actually ignored when the IME window 228 // does not have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS. We are calling setNavigationBarColor() 229 // to ensure it. 230 builder.setNavigationBarColor(navigationBarColor); 231 if (lightNavigationBar) { 232 // As documented, SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR is actually ignored when the IME 233 // window does not have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS. We set this flag just to 234 // ensure it. 235 builder.setInputViewSystemUiVisibility(SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 236 } 237 return builder; 238 } 239 240 @NonNull 241 private Bitmap getNavigationBarBitmap(@NonNull ImeSettings.Builder builder, 242 @ColorInt int appNavigationBarColor, boolean appLightNavigationBar, 243 int navigationBarHeight, @NonNull DimmingTestMode dimmingTestMode) 244 throws Exception { 245 final UiAutomation uiAutomation = 246 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 247 try (MockImeSession imeSession = MockImeSession.create( 248 InstrumentationRegistry.getContext(), uiAutomation, builder)) { 249 final ImeEventStream stream = imeSession.openEventStream(); 250 251 final TestActivity activity = launchTestActivity( 252 appNavigationBarColor, appLightNavigationBar, dimmingTestMode); 253 254 // Show AlertDialog if necessary, based on the dimming test mode. 255 try (AutoCloseable dialogCloser = showDialogIfNecessary( 256 activity, dimmingTestMode)) { 257 // Wait until the MockIme gets bound to the TestActivity. 258 expectBindInput(stream, Process.myPid(), TIMEOUT); 259 260 // Wait until "onStartInput" gets called for the EditText. 261 expectEvent(stream, event -> { 262 if (!TextUtils.equals("onStartInputView", event.getEventName())) { 263 return false; 264 } 265 final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo"); 266 return TextUtils.equals(TEST_MARKER, editorInfo.privateImeOptions); 267 }, TIMEOUT); 268 269 // Wait until MockIme's layout becomes stable. 270 final ImeLayoutInfo lastLayout = 271 waitForInputViewLayoutStable(stream, LAYOUT_STABLE_THRESHOLD); 272 assertNotNull(lastLayout); 273 274 final Bitmap bitmap = uiAutomation.takeScreenshot(); 275 return Bitmap.createBitmap(bitmap, 0, bitmap.getHeight() - navigationBarHeight, 276 bitmap.getWidth(), navigationBarHeight); 277 } 278 } 279 } 280 281 @Test 282 public void testSetNavigationBarColor() throws Exception { 283 final NavigationBarInfo info = NavigationBarInfo.getInstance(); 284 285 // Make sure that Window#setNavigationBarColor() works for IMEs. 286 expectNavigationBarColorSupported(color -> 287 getNavigationBarBitmap(imeSettingForSolidNavigationBar(color, false), 288 Color.BLACK, false, info.getBottomNavigationBerHeight(), 289 DimmingTestMode.NO_DIMMING_DIALOG)); 290 291 // Make sure that IME's navigation bar can be transparent 292 expectNavigationBarColorSupported(color -> 293 getNavigationBarBitmap(imeSettingForSolidNavigationBar(Color.TRANSPARENT, false), 294 color, false, info.getBottomNavigationBerHeight(), 295 DimmingTestMode.NO_DIMMING_DIALOG)); 296 297 // Make sure that Window#setNavigationBarColor() is ignored when 298 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is unset 299 expectNavigationBarColorNotSupported(color -> 300 getNavigationBarBitmap(imeSettingForFloatingIme(color, false), 301 Color.BLACK, false, info.getBottomNavigationBerHeight(), 302 DimmingTestMode.NO_DIMMING_DIALOG)); 303 } 304 305 @Test 306 public void testLightNavigationBar() throws Exception { 307 final NavigationBarInfo info = NavigationBarInfo.getInstance(); 308 309 assumeTrue("This test does not make sense if light navigation bar is not supported" 310 + " even for typical Activity", info.supportsLightNavigationBar()); 311 312 // Make sure that SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR works for IMEs (Bug 69002467). 313 expectLightNavigationBarSupported((color, lightMode) -> 314 getNavigationBarBitmap(imeSettingForSolidNavigationBar(color, lightMode), 315 Color.BLACK, false, info.getBottomNavigationBerHeight(), 316 DimmingTestMode.NO_DIMMING_DIALOG)); 317 318 // Make sure that IMEs can opt-out navigation bar custom rendering, including 319 // SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, by un-setting FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag 320 // so that it can be controlled by the target application instead (Bug 69111208). 321 expectLightNavigationBarSupported((color, lightMode) -> 322 getNavigationBarBitmap(imeSettingForFloatingIme(Color.BLACK, false), 323 color, lightMode, info.getBottomNavigationBerHeight(), 324 DimmingTestMode.NO_DIMMING_DIALOG)); 325 } 326 327 @Test 328 public void testDimmingWindow() throws Exception { 329 final NavigationBarInfo info = NavigationBarInfo.getInstance(); 330 331 assumeTrue("This test does not make sense if dimming windows do not affect light " 332 + " light navigation bar for typical Activities", 333 info.supportsDimmingWindowLightNavigationBarOverride()); 334 335 // Make sure that SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR works for IMEs, even if a dimming 336 // window is shown behind the IME window. 337 expectLightNavigationBarSupported((color, lightMode) -> 338 getNavigationBarBitmap(imeSettingForSolidNavigationBar(color, lightMode), 339 Color.BLACK, false, info.getBottomNavigationBerHeight(), 340 DimmingTestMode.DIMMING_DIALOG_BEHIND_IME)); 341 342 // If a dimming window is shown above the IME window, IME window's 343 // SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR should be canceled. 344 expectLightNavigationBarNotSupported((color, lightMode) -> 345 getNavigationBarBitmap(imeSettingForSolidNavigationBar(color, lightMode), 346 Color.BLACK, false, info.getBottomNavigationBerHeight(), 347 DimmingTestMode.DIMMING_DIALOG_ABOVE_IME)); 348 } 349 } 350