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