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