xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/SystemUi.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
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