All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowDisplayManager Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.P;
import static java.util.Objects.requireNonNull;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.content.Context;
import android.content.res.Configuration;
import android.hardware.display.BrightnessChangeEvent;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.auto.value.AutoBuilder;
import java.util.HashMap;
import java.util.List;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
import org.robolectric.android.internal.DisplayConfig;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.res.Qualifiers;
import org.robolectric.util.Consumer;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;

/**
 * For tests, display properties may be changed and devices may be added or removed
 * programmatically.
 */
@Implements(value = DisplayManager.class, minSdk = JELLY_BEAN_MR1, looseSignatures = true)
public class ShadowDisplayManager {

  @RealObject private DisplayManager realDisplayManager;

  private Context context;

  private static final HashMap displayIsNaturallyPortrait = new HashMap<>();

  @Resetter
  public static void reset() {
    displayIsNaturallyPortrait.clear();
  }

  @Implementation
  protected void __constructor__(Context context) {
    this.context = context;

    invokeConstructor(
        DisplayManager.class, realDisplayManager, ClassParameter.from(Context.class, context));
  }

  /**
   * Adds a simulated display and drain the main looper queue to ensure all the callbacks are
   * processed.
   *
   * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new
   *     display.
   * @return the new display's ID
   */
  public static int addDisplay(String qualifiersStr) {
    int id = getShadowDisplayManagerGlobal().addDisplay(createDisplayInfo(qualifiersStr, null));
    shadowMainLooper().idle();
    return id;
  }

  /** internal only */
  public static void configureDefaultDisplay(
      Configuration configuration, DisplayMetrics displayMetrics) {
    ShadowDisplayManagerGlobal shadowDisplayManagerGlobal = getShadowDisplayManagerGlobal();
    if (DisplayManagerGlobal.getInstance().getDisplayIds().length != 0) {
      throw new IllegalStateException("this method should only be called by Robolectric");
    }

    shadowDisplayManagerGlobal.addDisplay(
        createDisplayInfo(configuration, displayMetrics, /* isNaturallyPortrait= */ true));
  }

  private static DisplayInfo createDisplayInfo(
      Configuration configuration, DisplayMetrics displayMetrics, boolean isNaturallyPortrait) {
    int widthPx = (int) (configuration.screenWidthDp * displayMetrics.density);
    int heightPx = (int) (configuration.screenHeightDp * displayMetrics.density);

    DisplayInfo displayInfo = new DisplayInfo();
    displayInfo.name = "Built-in screen";
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      displayInfo.uniqueId = "screen0";
    }
    displayInfo.appWidth = widthPx;
    displayInfo.appHeight = heightPx;
    fixNominalDimens(displayInfo);
    displayInfo.logicalWidth = widthPx;
    displayInfo.logicalHeight = heightPx;
    displayInfo.rotation =
        configuration.orientation == ORIENTATION_PORTRAIT
            ? (isNaturallyPortrait ? Surface.ROTATION_0 : Surface.ROTATION_90)
            : (isNaturallyPortrait ? Surface.ROTATION_90 : Surface.ROTATION_0);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      displayInfo.modeId = 0;
      displayInfo.defaultModeId = 0;
      displayInfo.supportedModes = new Display.Mode[] {new Display.Mode(0, widthPx, heightPx, 60)};
    }
    displayInfo.logicalDensityDpi = displayMetrics.densityDpi;
    displayInfo.physicalXDpi = displayMetrics.densityDpi;
    displayInfo.physicalYDpi = displayMetrics.densityDpi;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      displayInfo.state = Display.STATE_ON;
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      displayInfo.getAppMetrics(displayMetrics);
    }

    return displayInfo;
  }

  private static DisplayInfo createDisplayInfo(String qualifiersStr, @Nullable Integer displayId) {
    DisplayInfo baseDisplayInfo =
        displayId != null ? DisplayManagerGlobal.getInstance().getDisplayInfo(displayId) : null;
    Configuration configuration = new Configuration();
    DisplayMetrics displayMetrics = new DisplayMetrics();

    boolean isNaturallyPortrait =
        requireNonNull(displayIsNaturallyPortrait.getOrDefault(displayId, true));
    if (qualifiersStr.startsWith("+") && baseDisplayInfo != null) {
      configuration.orientation =
          isRotated(baseDisplayInfo.rotation)
              ? (isNaturallyPortrait ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT)
              : (isNaturallyPortrait ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE);
      configuration.screenWidthDp =
          baseDisplayInfo.logicalWidth
              * DisplayMetrics.DENSITY_DEFAULT
              / baseDisplayInfo.logicalDensityDpi;
      configuration.screenHeightDp =
          baseDisplayInfo.logicalHeight
              * DisplayMetrics.DENSITY_DEFAULT
              / baseDisplayInfo.logicalDensityDpi;
      configuration.densityDpi = baseDisplayInfo.logicalDensityDpi;
      displayMetrics.densityDpi = baseDisplayInfo.logicalDensityDpi;
      displayMetrics.density =
          baseDisplayInfo.logicalDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
    }

    Bootstrap.applyQualifiers(
        qualifiersStr, RuntimeEnvironment.getApiLevel(), configuration, displayMetrics);

    return createDisplayInfo(configuration, displayMetrics, isNaturallyPortrait);
  }

  private static boolean isRotated(int rotation) {
    return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
  }

  private static void fixNominalDimens(DisplayInfo displayInfo) {
    int smallest = Math.min(displayInfo.appWidth, displayInfo.appHeight);
    int largest = Math.max(displayInfo.appWidth, displayInfo.appHeight);

    displayInfo.smallestNominalAppWidth = smallest;
    displayInfo.smallestNominalAppHeight = smallest;
    displayInfo.largestNominalAppWidth = largest;
    displayInfo.largestNominalAppHeight = largest;
  }

  /**
   * Changes properties of a simulated display. If {@param qualifiersStr} starts with a plus ('+')
   * sign, the display's previous configuration is modified with the given qualifiers; otherwise
   * defaults are applied as described here.
   *
   * 

Idles the main looper to ensure all listeners are notified. * * @param displayId the display id to change * @param qualifiersStr the {@link Qualifiers} string representing characteristics of the new * display */ public static void changeDisplay(int displayId, String qualifiersStr) { DisplayInfo displayInfo = createDisplayInfo(qualifiersStr, displayId); getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo); shadowMainLooper().idle(); } /** * Changes the display to be naturally portrait or landscape. This will ensure that the rotation * is configured consistently with orientation when the orientation is configured by {@link * #changeDisplay}, e.g. if the display is naturally portrait and the orientation is configured as * landscape the rotation will be set to {@link Surface#ROTATION_90}. */ public static void setNaturallyPortrait(int displayId, boolean isNaturallyPortrait) { displayIsNaturallyPortrait.put(displayId, isNaturallyPortrait); changeDisplay( displayId, config -> { boolean isRotated = isRotated(config.rotation); boolean isPortrait = config.logicalHeight > config.logicalWidth; if ((isNaturallyPortrait ^ isPortrait) != isRotated) { config.rotation = (isNaturallyPortrait ^ isPortrait) ? Surface.ROTATION_90 : Surface.ROTATION_0; } }); shadowMainLooper().idle(); } /** * Sets supported modes to the specified display with ID {@code displayId}. * *

Idles the main looper to ensure all listeners are notified. * * @param displayId the display id to change * @param supportedModes the display's supported modes */ public static void setSupportedModes(int displayId, Display.Mode... supportedModes) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { throw new UnsupportedOperationException("multiple display modes not supported before M"); } DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); displayInfo.supportedModes = supportedModes; getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo); shadowMainLooper().idle(); } /** * Changes properties of a simulated display. The original properties will be passed to the * {@param consumer}, which may modify them in place. The display will be updated with the new * properties. * * @param displayId the display id to change * @param consumer a function which modifies the display properties */ static void changeDisplay(int displayId, Consumer consumer) { DisplayInfo displayInfo = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); if (displayInfo != null) { DisplayConfig displayConfig = new DisplayConfig(displayInfo); consumer.accept(displayConfig); displayConfig.copyTo(displayInfo); fixNominalDimens(displayInfo); } getShadowDisplayManagerGlobal().changeDisplay(displayId, displayInfo); } /** * Removes a simulated display and idles the main looper to ensure all listeners are notified. * * @param displayId the display id to remove */ public static void removeDisplay(int displayId) { getShadowDisplayManagerGlobal().removeDisplay(displayId); shadowMainLooper().idle(); } /** * Returns the current display saturation level set via {@link * android.hardware.display.DisplayManager#setSaturationLevel(float)}. */ public float getSaturationLevel() { if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) { ShadowColorDisplayManager shadowCdm = extract(context.getSystemService(Context.COLOR_DISPLAY_SERVICE)); return shadowCdm.getSaturationLevel() / 100f; } return getShadowDisplayManagerGlobal().getSaturationLevel(); } /** * Sets the current display saturation level. * *

This is a workaround for tests which cannot use the relevant hidden {@link * android.annotation.SystemApi}, {@link * android.hardware.display.DisplayManager#setSaturationLevel(float)}. */ @Implementation(minSdk = P) public void setSaturationLevel(float level) { reflector(DisplayManagerReflector.class, realDisplayManager).setSaturationLevel(level); } @Implementation(minSdk = P) @HiddenApi protected void setBrightnessConfiguration(Object config) { setBrightnessConfigurationForUser(config, 0, context.getPackageName()); } @Implementation(minSdk = P) @HiddenApi protected void setBrightnessConfigurationForUser( Object config, Object userId, Object packageName) { getShadowDisplayManagerGlobal().setBrightnessConfigurationForUser(config, userId, packageName); } /** Set the default brightness configuration for this device. */ public static void setDefaultBrightnessConfiguration(Object config) { getShadowDisplayManagerGlobal().setDefaultBrightnessConfiguration(config); } /** Set the slider events the system has seen. */ public static void setBrightnessEvents(List events) { getShadowDisplayManagerGlobal().setBrightnessEvents(events); } private static ShadowDisplayManagerGlobal getShadowDisplayManagerGlobal() { if (Build.VERSION.SDK_INT < JELLY_BEAN_MR1) { throw new UnsupportedOperationException("multiple displays not supported in Jelly Bean"); } return extract(DisplayManagerGlobal.getInstance()); } @RequiresApi(api = Build.VERSION_CODES.M) static Display.Mode displayModeOf(int modeId, int width, int height, float refreshRate) { return new Display.Mode(modeId, width, height, refreshRate); } /** Builder class for {@link Display.Mode} */ @RequiresApi(api = Build.VERSION_CODES.M) @AutoBuilder(callMethod = "displayModeOf") public abstract static class ModeBuilder { public static ModeBuilder modeBuilder(int modeId) { return new AutoBuilder_ShadowDisplayManager_ModeBuilder().setModeId(modeId); } abstract ModeBuilder setModeId(int modeId); public abstract ModeBuilder setWidth(int width); public abstract ModeBuilder setHeight(int height); public abstract ModeBuilder setRefreshRate(float refreshRate); public abstract Display.Mode build(); } @ForType(DisplayManager.class) interface DisplayManagerReflector { @Direct void setSaturationLevel(float level); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy