1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.R; 4 import static java.lang.Math.max; 5 import static java.lang.Math.round; 6 7 import android.graphics.Rect; 8 import android.hardware.display.DisplayManagerGlobal; 9 import android.util.DisplayMetrics; 10 import android.view.Display; 11 import android.view.DisplayInfo; 12 import android.view.InsetsState; 13 import android.view.Surface; 14 import android.view.View; 15 import android.view.WindowInsets; 16 import android.view.WindowManager; 17 import com.google.common.collect.ImmutableList; 18 import java.util.ArrayList; 19 import java.util.List; 20 import javax.annotation.Nonnull; 21 import org.robolectric.RuntimeEnvironment; 22 import org.robolectric.shadow.api.Shadow; 23 import org.robolectric.shadows.ShadowWindowManagerGlobal.WindowInfo; 24 import org.robolectric.shadows.SystemUi.SystemBar.Side; 25 26 /** 27 * State holder for the Android system UI. 28 * 29 * <p>The system UI is configured per display and the system UI can be retrieved for the default 30 * display using {@link #systemUiForDefaultDisplay()} or for a display identified by its ID using 31 * {@link #systemUiForDisplay(int)}. 32 * 33 * <p>For backwards compatibility with previous Robolectric versions by default the system UIs are 34 * configured with no status bar or navigation insets, to apply a "standard" phone setup configure a 35 * status bar and navigation bar behavior e.g. in your test setup: 36 * 37 * <pre>{@code 38 * systemUiForDefaultDisplay() 39 * .setBehavior(SystemUi.STANDARD_STATUS_BAR, SystemUi.GESTURAL_NAVIGATION); 40 * }</pre> 41 * 42 * <p>{@link SystemUi} includes the most common Android system UI behaviors including: 43 * 44 * <ul> 45 * <li>{@link #NO_STATUS_BAR} - The default, no status bar insets reserved. 46 * <li>{@link #STANDARD_STATUS_BAR} - A standard status bar that grows if a top cutout is present. 47 * <li>{@link #GESTURAL_NAVIGATION} - Standard gestural navigation with bottom inset and gestural 48 * areas on the bottom and sides of the screen. 49 * <li>{@link #THREE_BUTTON_NAVIGATION} - Standard three button navigation bar that aligns to the 50 * bottom of the screen, and on smaller screens moves to the sides when rotated. 51 * <li>{@link #GESTURAL_NAVIGATION} - Standard two button navigation bar with similar alignment to 52 * the three button bar but also reserves a gestural area at the bottom of the screen. 53 * </ul> 54 * 55 * <p>It's recommended to use the predefined behaviors which attempt to align with real Android 56 * behavior, but if necessary custom system bar and navigation bar behaviors can be defined by 57 * implementing the {@link StatusBarBehavior} and {@link NavigationBarBehavior} interfaces 58 * respectively. 59 */ 60 // TODO: Make public when we're happy with the implementation/api/behavior 61 final class SystemUi { 62 /** Default status bar behavior which renders a 0 height status bar. */ 63 public static final StatusBarBehavior NO_STATUS_BAR = new NoStatusBarBehavior(); 64 65 /** Standard Android status bar behavior which behaves similarly to real Android. */ 66 public static final StatusBarBehavior STANDARD_STATUS_BAR = new StandardStatusBarBehavior(); 67 68 /** Default navigation bar behavior which renders a 0 height navigation bar. */ 69 public static final NavigationBarBehavior NO_NAVIGATION_BAR = new NoNavigationBarBehavior(); 70 71 /** Standard Android gestural navigation bar behavior. */ 72 public static final NavigationBarBehavior GESTURAL_NAVIGATION = 73 new GesturalNavigationBarBehavior(); 74 75 /** Standard Android three button navigation bar behavior. */ 76 public static final NavigationBarBehavior THREE_BUTTON_NAVIGATION = 77 new ButtonNavigationBarBehavior(); 78 79 private final int displayId; 80 private final StatusBar statusBar; 81 private final NavigationBar navigationBar; 82 private final ImmutableList<SystemBar> systemsBars; 83 84 interface OnChangeListener { onChange()85 void onChange(); 86 } 87 88 private final List<OnChangeListener> listeners = new ArrayList<>(); 89 90 /** Returns the {@link SystemUi} for the default display. */ systemUiForDefaultDisplay()91 public static SystemUi systemUiForDefaultDisplay() { 92 return systemUiForDisplay(Display.DEFAULT_DISPLAY); 93 } 94 95 /** Returns the {@link SystemUi} for the given display. */ systemUiForDisplay(int displayId)96 public static SystemUi systemUiForDisplay(int displayId) { 97 return Shadow.<ShadowDisplayManagerGlobal>extract(DisplayManagerGlobal.getInstance()) 98 .getSystemUi(displayId); 99 } 100 SystemUi(int displayId)101 SystemUi(int displayId) { 102 this.displayId = displayId; 103 statusBar = new StatusBar(displayId); 104 navigationBar = new NavigationBar(displayId); 105 systemsBars = ImmutableList.of(statusBar, navigationBar); 106 } 107 getDisplayId()108 int getDisplayId() { 109 return displayId; 110 } 111 addListener(OnChangeListener listener)112 void addListener(OnChangeListener listener) { 113 listeners.add(listener); 114 } 115 getStatusBar()116 public StatusBar getStatusBar() { 117 return statusBar; 118 } 119 120 /** Returns the status bar behavior. The default status bar behavior is {@link #NO_STATUS_BAR}. */ getStatusBarBehavior()121 public StatusBarBehavior getStatusBarBehavior() { 122 return statusBar.getBehavior(); 123 } 124 125 /** 126 * Sets the status bar behavior. 127 * 128 * <p>The default behavior is {@link #NO_STATUS_BAR}, use {@link #STANDARD_STATUS_BAR} for a 129 * standard Android status bar behavior. 130 */ setStatusBarBehavior(StatusBarBehavior statusBarBehavior)131 public void setStatusBarBehavior(StatusBarBehavior statusBarBehavior) { 132 statusBar.setBehavior(statusBarBehavior); 133 } 134 getNavigationBar()135 public NavigationBar getNavigationBar() { 136 return navigationBar; 137 } 138 139 /** 140 * Returns the navigation bar behavior. The default navigation bar behavior is {@link 141 * #NO_NAVIGATION_BAR}. 142 */ getNavigationBarBehavior()143 public NavigationBarBehavior getNavigationBarBehavior() { 144 return navigationBar.getBehavior(); 145 } 146 147 /** 148 * Sets the navigation bar behavior. 149 * 150 * <p>The default behavior is {@link #NO_NAVIGATION_BAR}, use {@link #GESTURAL_NAVIGATION} or 151 * {@link #THREE_BUTTON_NAVIGATION} for a standard on screen Android navigation bar behavior. 152 */ setNavigationBarBehavior(NavigationBarBehavior statusBarBehavior)153 public void setNavigationBarBehavior(NavigationBarBehavior statusBarBehavior) { 154 navigationBar.setBehavior(statusBarBehavior); 155 } 156 setBehavior( StatusBarBehavior statusBarBehavior, NavigationBarBehavior navigationBarBehavior)157 public void setBehavior( 158 StatusBarBehavior statusBarBehavior, NavigationBarBehavior navigationBarBehavior) { 159 setStatusBarBehavior(statusBarBehavior); 160 setNavigationBarBehavior(navigationBarBehavior); 161 } 162 163 @SuppressWarnings("deprecation") // Back compat support for system ui visibility adjustFrameForInsets(WindowManager.LayoutParams attrs, Rect outFrame)164 void adjustFrameForInsets(WindowManager.LayoutParams attrs, Rect outFrame) { 165 boolean hideStatusBar; 166 boolean hideNavigationBar; 167 if (RuntimeEnvironment.getApiLevel() >= R) { 168 hideStatusBar = (attrs.getFitInsetsTypes() & WindowInsets.Type.statusBars()) != 0; 169 hideNavigationBar = (attrs.getFitInsetsTypes() & WindowInsets.Type.navigationBars()) != 0; 170 } else { 171 int systemUiVisibility = attrs.systemUiVisibility | attrs.subtreeSystemUiVisibility; 172 hideStatusBar = 173 (systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 174 && (attrs.flags & WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN) == 0 175 && (attrs.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) == 0; 176 hideNavigationBar = 177 (systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 178 && (attrs.flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) == 0; 179 } 180 if (hideStatusBar) { 181 statusBar.insetFrame(outFrame); 182 } 183 if (hideNavigationBar) { 184 navigationBar.insetFrame(outFrame); 185 } 186 } 187 putInsets(WindowInfo windowInfo)188 void putInsets(WindowInfo windowInfo) { 189 putInsets(windowInfo, windowInfo.contentInsets, /* includeNotVisible= */ false); 190 putInsets(windowInfo, windowInfo.visibleInsets, /* includeNotVisible= */ false); 191 putInsets(windowInfo, windowInfo.stableInsets, /* includeNotVisible= */ true); 192 if (windowInfo.insetsState != null) { 193 putInsetsState(windowInfo, windowInfo.insetsState); 194 } 195 } 196 putInsets(WindowInfo info, Rect outInsets, boolean includeNotVisible)197 private void putInsets(WindowInfo info, Rect outInsets, boolean includeNotVisible) { 198 outInsets.set(0, 0, 0, 0); 199 for (SystemBar bar : systemsBars) { 200 if (includeNotVisible || bar.isVisible()) { 201 bar.putInsets(info.displayFrame, info.frame, outInsets); 202 } 203 } 204 } 205 putInsetsState(WindowInfo info, InsetsState outInsetsState)206 private void putInsetsState(WindowInfo info, InsetsState outInsetsState) { 207 outInsetsState.setDisplayFrame(info.frame); 208 ShadowInsetsState outShadowInsetsState = Shadow.extract(outInsetsState); 209 for (SystemBar bar : systemsBars) { 210 Shadow.<ShadowInsetsSource>extract(outShadowInsetsState.getOrCreateSource(bar.getId())) 211 .setFrame(bar.inFrame(info.displayFrame, info.frame)) 212 .setVisible(bar.isVisible()); 213 } 214 } 215 dpToPx(int px, int displayId)216 private static int dpToPx(int px, int displayId) { 217 return dpToPx(px, DisplayManagerGlobal.getInstance().getDisplayInfo(displayId)); 218 } 219 dpToPx(int px, DisplayInfo displayInfo)220 private static int dpToPx(int px, DisplayInfo displayInfo) { 221 float density = displayInfo.logicalDensityDpi / (float) DisplayMetrics.DENSITY_DEFAULT; 222 return round(density * px); 223 } 224 225 /** 226 * Base interface for behavior for a system bar such as status bar or navigation bar. See the 227 * specific interfaces {@link StatusBarBehavior} and {@link NavigationBarBehavior}. 228 */ 229 public interface SystemBarBehavior { 230 /** 231 * Returns which side of the screen this system bar should be attached to when rendered on the 232 * given display ID. The implementation may look up the size of the display to determine the 233 * side. 234 */ calculateSide(int displayId)235 Side calculateSide(int displayId); 236 237 /** 238 * Returns the size of the this system bar when rendered on the given display ID. This is either 239 * the height or the width based on the return value from {@link #calculateSide(int)}. The 240 * implementation may look up the size of the display to determine the side. 241 */ calculateSize(int displayId)242 int calculateSize(int displayId); 243 } 244 245 /** 246 * Interface for status bar behavior. See {@link #STANDARD_STATUS_BAR} and {@link #NO_STATUS_BAR} 247 * for default implementations. Custom status bar behavior can be provided by implementing this 248 * interface and calling {@link SystemUi#setStatusBarBehavior(StatusBarBehavior)}. 249 */ 250 public interface StatusBarBehavior extends SystemBarBehavior {} 251 252 /** 253 * Interface for navigation bar behavior. See {@link #GESTURAL_NAVIGATION}, {@link 254 * #THREE_BUTTON_NAVIGATION}, and {@link #NO_NAVIGATION_BAR} for default implementations. Custom 255 * status bar behavior can be provided by implementing this interface and calling {@link 256 * SystemUi#setNavigationBarBehavior(NavigationBarBehavior)}. 257 */ 258 public interface NavigationBarBehavior extends SystemBarBehavior {} 259 260 /** Base class for a system bar. See {@link StatusBar} and {@link NavigationBar}. */ 261 public abstract static class SystemBar { 262 /** Side of the screen a system bar is attached to. */ 263 public enum Side { 264 LEFT, 265 TOP, 266 RIGHT, 267 BOTTOM 268 } 269 SystemBar()270 SystemBar() {} 271 getId()272 abstract int getId(); 273 274 /** Returns which side of the screen this bar is attached to. */ getSide()275 public abstract Side getSide(); 276 277 /** 278 * Returns the size of this status bar. Depending on which side of the screen the bar is 279 * attached to this is either the height (for top and bottom) or width (for left or right). 280 */ getSize()281 public abstract int getSize(); 282 283 /** 284 * Returns true if this status bar is currently visible. Note that this is still tracked even if 285 * the status bar has 0 size. 286 */ isVisible()287 public abstract boolean isVisible(); 288 insetFrame(Rect outFrame)289 void insetFrame(Rect outFrame) { 290 switch (getSide()) { 291 case LEFT: 292 outFrame.left += getSize(); 293 break; 294 case TOP: 295 outFrame.top += getSize(); 296 break; 297 case RIGHT: 298 outFrame.right -= getSize(); 299 break; 300 case BOTTOM: 301 outFrame.bottom -= getSize(); 302 break; 303 } 304 } 305 inFrame(Rect displayFrame, Rect frame)306 Rect inFrame(Rect displayFrame, Rect frame) { 307 switch (getSide()) { 308 case LEFT: 309 return new Rect(0, 0, max(0, getSize() - frame.left), frame.bottom); 310 case TOP: 311 return new Rect(0, 0, frame.right, max(0, getSize() - frame.top)); 312 case RIGHT: 313 int rightSize = max(0, getSize() - (displayFrame.right - frame.right)); 314 return new Rect(frame.right - rightSize, 0, frame.right, frame.bottom); 315 case BOTTOM: 316 int bottomSize = max(0, getSize() - (displayFrame.bottom - frame.bottom)); 317 return new Rect(0, frame.bottom - bottomSize, frame.right, frame.bottom); 318 } 319 throw new IllegalStateException(); 320 } 321 putInsets(Rect displayFrame, Rect frame, Rect insets)322 void putInsets(Rect displayFrame, Rect frame, Rect insets) { 323 switch (getSide()) { 324 case LEFT: 325 insets.left = max(insets.left, getSize() - frame.left); 326 break; 327 case TOP: 328 insets.top = max(insets.top, getSize() - frame.top); 329 break; 330 case RIGHT: 331 insets.right = max(insets.right, getSize() - (displayFrame.right - frame.right)); 332 break; 333 case BOTTOM: 334 insets.bottom = max(insets.bottom, getSize() - (displayFrame.bottom - frame.bottom)); 335 break; 336 } 337 } 338 } 339 340 /** Represents the system status bar. */ 341 public static final class StatusBar extends SystemBar { 342 private final int displayId; 343 private StatusBarBehavior behavior = NO_STATUS_BAR; 344 private boolean isVisible = true; 345 StatusBar(int displayId)346 StatusBar(int displayId) { 347 this.displayId = displayId; 348 } 349 350 @Override getId()351 int getId() { 352 return ShadowInsetsState.STATUS_BARS; 353 } 354 getBehavior()355 StatusBarBehavior getBehavior() { 356 return behavior; 357 } 358 setBehavior(StatusBarBehavior behavior)359 void setBehavior(StatusBarBehavior behavior) { 360 this.behavior = behavior; 361 } 362 363 @Override isVisible()364 public boolean isVisible() { 365 return isVisible; 366 } 367 setVisible(boolean isVisible)368 boolean setVisible(boolean isVisible) { 369 boolean didChange = this.isVisible != isVisible; 370 this.isVisible = isVisible; 371 return didChange; 372 } 373 374 @Override getSide()375 public Side getSide() { 376 return behavior.calculateSide(displayId); 377 } 378 379 @Override getSize()380 public int getSize() { 381 return behavior.calculateSize(displayId); 382 } 383 384 @Nonnull 385 @Override toString()386 public String toString() { 387 return "StatusBar{isVisible=" + isVisible + "}"; 388 } 389 } 390 391 static final class NoStatusBarBehavior implements StatusBarBehavior { 392 @Override calculateSide(int displayId)393 public Side calculateSide(int displayId) { 394 return Side.TOP; 395 } 396 397 @Override calculateSize(int displayId)398 public int calculateSize(int displayId) { 399 return 0; 400 } 401 } 402 403 static final class StandardStatusBarBehavior implements StatusBarBehavior { 404 private static final int HEIGHT_DP = 24; 405 406 @Override calculateSide(int displayId)407 public Side calculateSide(int displayId) { 408 return Side.TOP; 409 } 410 411 @Override calculateSize(int displayId)412 public int calculateSize(int displayId) { 413 return dpToPx(HEIGHT_DP, displayId); 414 } 415 } 416 417 /** Represents the system navigation bar. */ 418 public static final class NavigationBar extends SystemBar { 419 private final int displayId; 420 private NavigationBarBehavior behavior = NO_NAVIGATION_BAR; 421 private boolean isVisible = true; 422 NavigationBar(int displayId)423 NavigationBar(int displayId) { 424 this.displayId = displayId; 425 } 426 427 @Override getId()428 int getId() { 429 return ShadowInsetsState.NAVIGATION_BARS; 430 } 431 getBehavior()432 NavigationBarBehavior getBehavior() { 433 return behavior; 434 } 435 setBehavior(NavigationBarBehavior behavior)436 void setBehavior(NavigationBarBehavior behavior) { 437 this.behavior = behavior; 438 } 439 440 @Override isVisible()441 public boolean isVisible() { 442 return isVisible; 443 } 444 setVisible(boolean isVisible)445 boolean setVisible(boolean isVisible) { 446 boolean didChange = this.isVisible != isVisible; 447 this.isVisible = isVisible; 448 return didChange; 449 } 450 451 @Override getSide()452 public Side getSide() { 453 return behavior.calculateSide(displayId); 454 } 455 456 @Override getSize()457 public int getSize() { 458 return behavior.calculateSize(displayId); 459 } 460 461 @Nonnull 462 @Override toString()463 public String toString() { 464 return "NavigationBar{isVisible=" + isVisible + "}"; 465 } 466 } 467 468 private static class NoNavigationBarBehavior implements NavigationBarBehavior { 469 @Override calculateSide(int displayId)470 public Side calculateSide(int displayId) { 471 return Side.BOTTOM; 472 } 473 474 @Override calculateSize(int displayId)475 public int calculateSize(int displayId) { 476 return 0; 477 } 478 } 479 480 private static class GesturalNavigationBarBehavior implements NavigationBarBehavior { 481 private static final int HEIGHT_DP = 24; 482 483 @Override calculateSide(int displayId)484 public Side calculateSide(int displayId) { 485 return Side.BOTTOM; 486 } 487 488 @Override calculateSize(int displayId)489 public int calculateSize(int displayId) { 490 return dpToPx(HEIGHT_DP, displayId); 491 } 492 } 493 494 private static class ButtonNavigationBarBehavior implements NavigationBarBehavior { 495 private static final int BOTTOM_HEIGHT_DP = 48; 496 private static final int SIDE_HEIGHT_DP = 42; 497 private static final int LARGE_SCREEN_DP = 600; 498 private static final int LARGE_SCREEN_HEIGHT_DP = 56; 499 500 @Override calculateSide(int displayId)501 public Side calculateSide(int displayId) { 502 return calculateSide(DisplayManagerGlobal.getInstance().getDisplayInfo(displayId)); 503 } 504 calculateSide(DisplayInfo info)505 private Side calculateSide(DisplayInfo info) { 506 if (isLargeScreen(info)) { 507 return Side.BOTTOM; 508 } else { 509 switch (info.rotation) { 510 case Surface.ROTATION_90: 511 return Side.LEFT; 512 case Surface.ROTATION_180: 513 return Side.RIGHT; 514 default: 515 return Side.BOTTOM; 516 } 517 } 518 } 519 520 @Override calculateSize(int displayId)521 public int calculateSize(int displayId) { 522 DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); 523 int sizeDp = 524 isLargeScreen(displayInfo) 525 ? LARGE_SCREEN_HEIGHT_DP 526 : (calculateSide(displayInfo) == Side.BOTTOM ? BOTTOM_HEIGHT_DP : SIDE_HEIGHT_DP); 527 return dpToPx(sizeDp, displayInfo); 528 } 529 isLargeScreen(DisplayInfo info)530 private boolean isLargeScreen(DisplayInfo info) { 531 return max(info.logicalWidth, info.logicalHeight) >= dpToPx(LARGE_SCREEN_DP, info); 532 } 533 } 534 } 535