1 package org.robolectric.shadows;
2 
3 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
4 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
5 import static android.os.Build.VERSION_CODES.P;
6 import static java.util.Objects.requireNonNull;
7 import static org.robolectric.shadow.api.Shadow.extract;
8 import static org.robolectric.shadow.api.Shadow.invokeConstructor;
9 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
10 import static org.robolectric.util.reflector.Reflector.reflector;
11 
12 import android.annotation.Nullable;
13 import android.annotation.RequiresApi;
14 import android.content.Context;
15 import android.content.res.Configuration;
16 import android.hardware.display.BrightnessChangeEvent;
17 import android.hardware.display.DisplayManager;
18 import android.hardware.display.DisplayManagerGlobal;
19 import android.os.Build;
20 import android.util.DisplayMetrics;
21 import android.view.Display;
22 import android.view.DisplayInfo;
23 import android.view.Surface;
24 import com.google.auto.value.AutoBuilder;
25 import java.util.HashMap;
26 import java.util.List;
27 import org.robolectric.RuntimeEnvironment;
28 import org.robolectric.android.Bootstrap;
29 import org.robolectric.android.internal.DisplayConfig;
30 import org.robolectric.annotation.ClassName;
31 import org.robolectric.annotation.HiddenApi;
32 import org.robolectric.annotation.Implementation;
33 import org.robolectric.annotation.Implements;
34 import org.robolectric.annotation.RealObject;
35 import org.robolectric.annotation.Resetter;
36 import org.robolectric.res.Qualifiers;
37 import org.robolectric.util.Consumer;
38 import org.robolectric.util.ReflectionHelpers;
39 import org.robolectric.util.ReflectionHelpers.ClassParameter;
40 import org.robolectric.util.reflector.Direct;
41 import org.robolectric.util.reflector.ForType;
42 import org.robolectric.versioning.AndroidVersions.V;
43 
44 /**
45  * For tests, display properties may be changed and devices may be added or removed
46  * programmatically.
47  */
48 @Implements(value = DisplayManager.class)
49 public class ShadowDisplayManager {
50 
51   @RealObject private DisplayManager realDisplayManager;
52 
53   private Context context;
54 
55   private static final String DEFAULT_DISPLAY_NAME = "Built-in screen";
56   private static final int DEFAULT_DISPLAY_TYPE = Display.TYPE_UNKNOWN;
57 
58   private static final HashMap<Integer, Boolean> displayIsNaturallyPortrait = new HashMap<>();
59 
60   @Resetter
reset()61   public static void reset() {
62     displayIsNaturallyPortrait.clear();
63   }
64 
65   @Implementation
__constructor__(Context context)66   protected void __constructor__(Context context) {
67     this.context = context;
68 
69     invokeConstructor(
70         DisplayManager.class, realDisplayManager, ClassParameter.from(Context.class, context));
71   }
72 
73   /**
74    * Adds a simulated display and drain the main looper queue to ensure all the callbacks are
75    * processed.
76    *
77    * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new
78    *     display.
79    * @return the new display's ID
80    */
addDisplay(String qualifiersStr)81   public static int addDisplay(String qualifiersStr) {
82     return addDisplayInternal(qualifiersStr, DEFAULT_DISPLAY_NAME, DEFAULT_DISPLAY_TYPE);
83   }
84 
85   /**
86    * Adds a physical display with given type and drain the main looper queue to ensure all the
87    * callbacks are processed.
88    *
89    * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new
90    *     display.
91    * @param displayType the integer denoting the type of the new display.
92    * @return the new display's ID
93    */
addDisplay(String qualifiersStr, int displayType)94   public static int addDisplay(String qualifiersStr, int displayType) {
95     return addDisplayInternal(qualifiersStr, DEFAULT_DISPLAY_NAME, displayType);
96   }
97 
98   /**
99    * Adds a simulated display and drain the main looper queue to ensure all the callbacks are
100    * processed.
101    *
102    * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new
103    *     display.
104    * @param displayName the display name to use while creating the display
105    * @return the new display's ID
106    */
addDisplay(String qualifiersStr, String displayName)107   public static int addDisplay(String qualifiersStr, String displayName) {
108     return addDisplayInternal(qualifiersStr, displayName, DEFAULT_DISPLAY_TYPE);
109   }
110 
111   static IllegalStateException configureDefaultDisplayCallstack;
112 
113   /** internal only */
configureDefaultDisplay( Configuration configuration, DisplayMetrics displayMetrics)114   public static void configureDefaultDisplay(
115       Configuration configuration, DisplayMetrics displayMetrics) {
116     ShadowDisplayManagerGlobal shadowDisplayManagerGlobal = getShadowDisplayManagerGlobal();
117     if (DisplayManagerGlobal.getInstance().getDisplayIds().length == 0) {
118       configureDefaultDisplayCallstack =
119           new IllegalStateException("configureDefaultDisplay should only be called once");
120     } else {
121       configureDefaultDisplayCallstack.initCause(
122           new IllegalStateException(
123               "configureDefaultDisplay was called a second time",
124               configureDefaultDisplayCallstack));
125       throw configureDefaultDisplayCallstack;
126     }
127 
128     shadowDisplayManagerGlobal.addDisplay(
129         createDisplayInfo(
130             configuration,
131             displayMetrics,
132             /* isNaturallyPortrait= */ true,
133             DEFAULT_DISPLAY_NAME,
134             DEFAULT_DISPLAY_TYPE));
135   }
136 
addDisplayInternal(String qualifiersStr, String displayName, int displayType)137   private static int addDisplayInternal(String qualifiersStr, String displayName, int displayType) {
138     int id =
139         getShadowDisplayManagerGlobal()
140             .addDisplay(createDisplayInfo(qualifiersStr, null, displayName, displayType));
141     shadowMainLooper().idle();
142     return id;
143   }
144 
createDisplayInfo( Configuration configuration, DisplayMetrics displayMetrics, boolean isNaturallyPortrait, String name, int displayType)145   private static DisplayInfo createDisplayInfo(
146       Configuration configuration,
147       DisplayMetrics displayMetrics,
148       boolean isNaturallyPortrait,
149       String name,
150       int displayType) {
151     int widthPx = (int) (configuration.screenWidthDp * displayMetrics.density);
152     int heightPx = (int) (configuration.screenHeightDp * displayMetrics.density);
153 
154     DisplayInfo displayInfo = new DisplayInfo();
155     displayInfo.name = name;
156     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
157       displayInfo.uniqueId = "screen0";
158     }
159     displayInfo.appWidth = widthPx;
160     displayInfo.appHeight = heightPx;
161     fixNominalDimens(displayInfo);
162     displayInfo.logicalWidth = widthPx;
163     displayInfo.logicalHeight = heightPx;
164     displayInfo.rotation =
165         configuration.orientation == ORIENTATION_PORTRAIT
166             ? (isNaturallyPortrait ? Surface.ROTATION_0 : Surface.ROTATION_90)
167             : (isNaturallyPortrait ? Surface.ROTATION_90 : Surface.ROTATION_0);
168     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
169       displayInfo.modeId = 0;
170       displayInfo.defaultModeId = 0;
171       displayInfo.supportedModes = new Display.Mode[] {new Display.Mode(0, widthPx, heightPx, 60)};
172     }
173     displayInfo.logicalDensityDpi = displayMetrics.densityDpi;
174     displayInfo.physicalXDpi = displayMetrics.densityDpi;
175     displayInfo.physicalYDpi = displayMetrics.densityDpi;
176     displayInfo.state = Display.STATE_ON;
177     displayInfo.type = displayType;
178 
179     return displayInfo;
180   }
181 
createDisplayInfo(String qualifiersStr, @Nullable Integer displayId)182   private static DisplayInfo createDisplayInfo(String qualifiersStr, @Nullable Integer displayId) {
183     return createDisplayInfo(qualifiersStr, displayId, DEFAULT_DISPLAY_NAME, DEFAULT_DISPLAY_TYPE);
184   }
185 
createDisplayInfo( String qualifiersStr, @Nullable Integer displayId, String name, int displayType)186   private static DisplayInfo createDisplayInfo(
187       String qualifiersStr, @Nullable Integer displayId, String name, int displayType) {
188     DisplayInfo baseDisplayInfo =
189         displayId != null ? DisplayManagerGlobal.getInstance().getDisplayInfo(displayId) : null;
190     Configuration configuration = new Configuration();
191     DisplayMetrics displayMetrics = new DisplayMetrics();
192 
193     boolean isNaturallyPortrait =
194         requireNonNull(displayIsNaturallyPortrait.getOrDefault(displayId, true));
195     if (qualifiersStr.startsWith("+") && baseDisplayInfo != null) {
196       configuration.orientation =
197           isRotated(baseDisplayInfo.rotation)
198               ? (isNaturallyPortrait ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT)
199               : (isNaturallyPortrait ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE);
200       configuration.screenWidthDp =
201           baseDisplayInfo.logicalWidth
202               * DisplayMetrics.DENSITY_DEFAULT
203               / baseDisplayInfo.logicalDensityDpi;
204       configuration.screenHeightDp =
205           baseDisplayInfo.logicalHeight
206               * DisplayMetrics.DENSITY_DEFAULT
207               / baseDisplayInfo.logicalDensityDpi;
208       configuration.densityDpi = baseDisplayInfo.logicalDensityDpi;
209       displayMetrics.densityDpi = baseDisplayInfo.logicalDensityDpi;
210       displayMetrics.density =
211           baseDisplayInfo.logicalDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
212     }
213 
214     Bootstrap.applyQualifiers(
215         qualifiersStr, RuntimeEnvironment.getApiLevel(), configuration, displayMetrics);
216 
217     return createDisplayInfo(configuration, displayMetrics, isNaturallyPortrait, name, displayType);
218   }
219 
isRotated(int rotation)220   private static boolean isRotated(int rotation) {
221     return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
222   }
223 
fixNominalDimens(DisplayInfo displayInfo)224   private static void fixNominalDimens(DisplayInfo displayInfo) {
225     int smallest = Math.min(displayInfo.appWidth, displayInfo.appHeight);
226     int largest = Math.max(displayInfo.appWidth, displayInfo.appHeight);
227 
228     displayInfo.smallestNominalAppWidth = smallest;
229     displayInfo.smallestNominalAppHeight = smallest;
230     displayInfo.largestNominalAppWidth = largest;
231     displayInfo.largestNominalAppHeight = largest;
232   }
233 
234   /**
235    * Changes properties of a simulated display. If {@param qualifiersStr} starts with a plus ('+')
236    * sign, the display's previous configuration is modified with the given qualifiers; otherwise
237    * defaults are applied as described <a
238    * href="http://robolectric.org/device-configuration/">here</a>.
239    *
240    * <p>Idles the main looper to ensure all listeners are notified.
241    *
242    * @param displayId the display id to change
243    * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new
244    *     display
245    */
changeDisplay(int displayId, String qualifiersStr)246   public static void changeDisplay(int displayId, String qualifiersStr) {
247     DisplayInfo displayInfo = createDisplayInfo(qualifiersStr, displayId);
248     getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo);
249     shadowMainLooper().idle();
250   }
251 
252   /**
253    * Changes the display to be naturally portrait or landscape. This will ensure that the rotation
254    * is configured consistently with orientation when the orientation is configured by {@link
255    * #changeDisplay}, e.g. if the display is naturally portrait and the orientation is configured as
256    * landscape the rotation will be set to {@link Surface#ROTATION_90}.
257    */
setNaturallyPortrait(int displayId, boolean isNaturallyPortrait)258   public static void setNaturallyPortrait(int displayId, boolean isNaturallyPortrait) {
259     displayIsNaturallyPortrait.put(displayId, isNaturallyPortrait);
260     changeDisplay(
261         displayId,
262         config -> {
263           boolean isRotated = isRotated(config.rotation);
264           boolean isPortrait = config.logicalHeight > config.logicalWidth;
265           if ((isNaturallyPortrait ^ isPortrait) != isRotated) {
266             config.rotation =
267                 (isNaturallyPortrait ^ isPortrait) ? Surface.ROTATION_90 : Surface.ROTATION_0;
268           }
269         });
270     shadowMainLooper().idle();
271   }
272 
273   /**
274    * Sets supported modes to the specified display with ID {@code displayId}.
275    *
276    * <p>Idles the main looper to ensure all listeners are notified.
277    *
278    * @param displayId the display id to change
279    * @param supportedModes the display's supported modes
280    */
setSupportedModes(int displayId, Display.Mode... supportedModes)281   public static void setSupportedModes(int displayId, Display.Mode... supportedModes) {
282     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
283       throw new UnsupportedOperationException("multiple display modes not supported before M");
284     }
285     DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
286     if (RuntimeEnvironment.getApiLevel() >= V.SDK_INT) {
287       ReflectionHelpers.setField(displayInfo, "appsSupportedModes", supportedModes);
288     } else {
289       displayInfo.supportedModes = supportedModes;
290     }
291     getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo);
292     shadowMainLooper().idle();
293   }
294 
295   /**
296    * Changes properties of a simulated display. The original properties will be passed to the
297    * {@param consumer}, which may modify them in place. The display will be updated with the new
298    * properties.
299    *
300    * @param displayId the display id to change
301    * @param consumer a function which modifies the display properties
302    */
changeDisplay(int displayId, Consumer<DisplayConfig> consumer)303   static void changeDisplay(int displayId, Consumer<DisplayConfig> consumer) {
304     DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
305     if (displayInfo != null) {
306       DisplayConfig displayConfig = new DisplayConfig(displayInfo);
307       consumer.accept(displayConfig);
308       displayConfig.copyTo(displayInfo);
309       fixNominalDimens(displayInfo);
310     }
311 
312     getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo);
313   }
314 
315   /**
316    * Removes a simulated display and idles the main looper to ensure all listeners are notified.
317    *
318    * @param displayId the display id to remove
319    */
removeDisplay(int displayId)320   public static void removeDisplay(int displayId) {
321     getShadowDisplayManagerGlobal().removeDisplay(displayId);
322     shadowMainLooper().idle();
323   }
324 
325   /**
326    * Returns the current display saturation level set via {@link
327    * android.hardware.display.DisplayManager#setSaturationLevel(float)}.
328    */
getSaturationLevel()329   public float getSaturationLevel() {
330     if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
331       ShadowColorDisplayManager shadowCdm =
332           extract(context.getSystemService(Context.COLOR_DISPLAY_SERVICE));
333       return shadowCdm.getSaturationLevel() / 100f;
334     }
335     return getShadowDisplayManagerGlobal().getSaturationLevel();
336   }
337 
338   /**
339    * Sets the current display saturation level.
340    *
341    * <p>This is a workaround for tests which cannot use the relevant hidden {@link
342    * android.annotation.SystemApi}, {@link
343    * android.hardware.display.DisplayManager#setSaturationLevel(float)}.
344    */
345   @Implementation(minSdk = P)
setSaturationLevel(float level)346   public void setSaturationLevel(float level) {
347     reflector(DisplayManagerReflector.class, realDisplayManager).setSaturationLevel(level);
348   }
349 
350   @Implementation(minSdk = P)
351   @HiddenApi
setBrightnessConfiguration( @lassName"android.hardware.display.BrightnessConfiguration") Object config)352   protected void setBrightnessConfiguration(
353       @ClassName("android.hardware.display.BrightnessConfiguration") Object config) {
354     setBrightnessConfigurationForUser(config, 0, context.getPackageName());
355   }
356 
357   @Implementation(minSdk = P)
358   @HiddenApi
setBrightnessConfigurationForUser( @lassName"android.hardware.display.BrightnessConfiguration") Object config, int userId, String packageName)359   protected void setBrightnessConfigurationForUser(
360       @ClassName("android.hardware.display.BrightnessConfiguration") Object config,
361       int userId,
362       String packageName) {
363     getShadowDisplayManagerGlobal().setBrightnessConfigurationForUser(config, userId, packageName);
364   }
365 
366   /** Set the default brightness configuration for this device. */
setDefaultBrightnessConfiguration(Object config)367   public static void setDefaultBrightnessConfiguration(Object config) {
368     getShadowDisplayManagerGlobal().setDefaultBrightnessConfiguration(config);
369   }
370 
371   /** Set the slider events the system has seen. */
setBrightnessEvents(List<BrightnessChangeEvent> events)372   public static void setBrightnessEvents(List<BrightnessChangeEvent> events) {
373     getShadowDisplayManagerGlobal().setBrightnessEvents(events);
374   }
375 
getShadowDisplayManagerGlobal()376   private static ShadowDisplayManagerGlobal getShadowDisplayManagerGlobal() {
377     return extract(DisplayManagerGlobal.getInstance());
378   }
379 
380   @RequiresApi(api = Build.VERSION_CODES.M)
displayModeOf(int modeId, int width, int height, float refreshRate)381   static Display.Mode displayModeOf(int modeId, int width, int height, float refreshRate) {
382     return new Display.Mode(modeId, width, height, refreshRate);
383   }
384 
385   /** Builder class for {@link Display.Mode} */
386   @RequiresApi(api = Build.VERSION_CODES.M)
387   @AutoBuilder(callMethod = "displayModeOf")
388   public abstract static class ModeBuilder {
modeBuilder(int modeId)389     public static ModeBuilder modeBuilder(int modeId) {
390       return new AutoBuilder_ShadowDisplayManager_ModeBuilder().setModeId(modeId);
391     }
392 
setModeId(int modeId)393     abstract ModeBuilder setModeId(int modeId);
394 
setWidth(int width)395     public abstract ModeBuilder setWidth(int width);
396 
setHeight(int height)397     public abstract ModeBuilder setHeight(int height);
398 
setRefreshRate(float refreshRate)399     public abstract ModeBuilder setRefreshRate(float refreshRate);
400 
build()401     public abstract Display.Mode build();
402   }
403 
404   @ForType(DisplayManager.class)
405   interface DisplayManagerReflector {
406 
407     @Direct
setSaturationLevel(float level)408     void setSaturationLevel(float level);
409   }
410 }
411