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