diff options
16 files changed, 1082 insertions, 48 deletions
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 512f4f2b5d22..981911ec8880 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -16,6 +16,7 @@ package android.view; +import static android.view.Display.Mode.INVALID_MODE_ID; import static android.view.DisplayInfoProto.APP_HEIGHT; import static android.view.DisplayInfoProto.APP_WIDTH; import static android.view.DisplayInfoProto.CUTOUT; @@ -200,6 +201,11 @@ public final class DisplayInfo implements Parcelable { public int defaultModeId; /** + * The user preferred display mode. + */ + public int userPreferredModeId = INVALID_MODE_ID; + + /** * The supported modes of this display. */ public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY; @@ -420,6 +426,7 @@ public final class DisplayInfo implements Parcelable { && modeId == other.modeId && renderFrameRate == other.renderFrameRate && defaultModeId == other.defaultModeId + && userPreferredModeId == other.userPreferredModeId && Arrays.equals(supportedModes, other.supportedModes) && colorMode == other.colorMode && Arrays.equals(supportedColorModes, other.supportedColorModes) @@ -478,6 +485,7 @@ public final class DisplayInfo implements Parcelable { modeId = other.modeId; renderFrameRate = other.renderFrameRate; defaultModeId = other.defaultModeId; + userPreferredModeId = other.userPreferredModeId; supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length); colorMode = other.colorMode; supportedColorModes = Arrays.copyOf( @@ -530,6 +538,7 @@ public final class DisplayInfo implements Parcelable { modeId = source.readInt(); renderFrameRate = source.readFloat(); defaultModeId = source.readInt(); + userPreferredModeId = source.readInt(); int nModes = source.readInt(); supportedModes = new Display.Mode[nModes]; for (int i = 0; i < nModes; i++) { @@ -596,6 +605,7 @@ public final class DisplayInfo implements Parcelable { dest.writeInt(modeId); dest.writeFloat(renderFrameRate); dest.writeInt(defaultModeId); + dest.writeInt(userPreferredModeId); dest.writeInt(supportedModes.length); for (int i = 0; i < supportedModes.length; i++) { supportedModes[i].writeToParcel(dest, flags); @@ -832,9 +842,12 @@ public final class DisplayInfo implements Parcelable { sb.append(presentationDeadlineNanos); sb.append(", mode "); sb.append(modeId); + sb.append(", renderFrameRate "); sb.append(renderFrameRate); sb.append(", defaultMode "); sb.append(defaultModeId); + sb.append(", userPreferredModeId "); + sb.append(userPreferredModeId); sb.append(", modes "); sb.append(Arrays.toString(supportedModes)); sb.append(", hdrCapabilities "); diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 4ff30f061332..a1adfc304e73 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5227,6 +5227,28 @@ non-zero. --> <integer name="config_defaultPeakRefreshRate">0</integer> + <!-- External display peak refresh rate for the given device. Change this value if you want to + prevent the framework from using higher refresh rates, even if display modes with higher + refresh rates are available from hardware composer. Only has an effect if this value and + config_externalDisplayPeakWidth and config_externalDisplayPeakHeight are non-zero. --> + <integer name="config_externalDisplayPeakRefreshRate">0</integer> + + <!-- External display peak width for the given device. Change this value if you want + to prevent the framework from using higher resolution, even if display modes with higher + resolutions are available from hardware composer. Only has an effect if this value and + config_externalDisplayPeakRefreshRate and config_externalDisplayPeakHeight are non-zero.--> + <integer name="config_externalDisplayPeakWidth">0</integer> + + <!-- External display peak height for the given device. Change this value if you want + to prevent the framework from using higher resolution, even if display modes with higher + resolutions are available from hardware composer. Only has an effect if this value and + config_externalDisplayPeakRefreshRate and config_externalDisplayPeakWidth are non-zero. --> + <integer name="config_externalDisplayPeakHeight">0</integer> + + <!-- Enable synchronization of the displays refresh rates by applying the default low refresh + rate. --> + <bool name="config_refreshRateSynchronizationEnabled">false</bool> + <!-- The display uses different gamma curves for different refresh rates. It's hard for panel vendors to tune the curves to have exact same brightness for different refresh rate. So flicker could be observed at switch time. The issue is worse at the gamma lower end. diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 83fb0986a19f..b0eee1cecc89 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4231,6 +4231,10 @@ <!-- For high refresh rate displays --> <java-symbol type="integer" name="config_defaultRefreshRate" /> <java-symbol type="integer" name="config_defaultPeakRefreshRate" /> + <java-symbol type="integer" name="config_externalDisplayPeakRefreshRate" /> + <java-symbol type="integer" name="config_externalDisplayPeakWidth" /> + <java-symbol type="integer" name="config_externalDisplayPeakHeight" /> + <java-symbol type="bool" name="config_refreshRateSynchronizationEnabled" /> <java-symbol type="integer" name="config_defaultRefreshRateInZone" /> <java-symbol type="array" name="config_brightnessThresholdsOfPeakRefreshRate" /> <java-symbol type="array" name="config_ambientThresholdsOfPeakRefreshRate" /> diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 507ae2676b16..9e92c8d7342d 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -500,6 +500,8 @@ public class DisplayDeviceConfig { public static final String DEFAULT_ID = "default"; + public static final int DEFAULT_LOW_REFRESH_RATE = 60; + private static final float BRIGHTNESS_DEFAULT = 0.5f; private static final String ETC_DIR = "etc"; private static final String DISPLAY_CONFIG_DIR = "displayconfig"; @@ -513,7 +515,6 @@ public class DisplayDeviceConfig { private static final int DEFAULT_PEAK_REFRESH_RATE = 0; private static final int DEFAULT_REFRESH_RATE = 60; private static final int DEFAULT_REFRESH_RATE_IN_HBM = 0; - private static final int DEFAULT_LOW_REFRESH_RATE = 60; private static final int DEFAULT_HIGH_REFRESH_RATE = 0; private static final float[] DEFAULT_BRIGHTNESS_THRESHOLDS = new float[]{}; diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 213ee646fc34..3529b048bd34 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -16,6 +16,8 @@ package com.android.server.display; +import static android.view.Display.Mode.INVALID_MODE_ID; + import android.hardware.display.DeviceProductInfo; import android.hardware.display.DisplayViewport; import android.util.DisplayMetrics; @@ -275,6 +277,11 @@ final class DisplayDeviceInfo { public int defaultModeId; /** + * The mode of the display which is preferred by user. + */ + public int userPreferredModeId = INVALID_MODE_ID; + + /** * The supported modes of the display. */ public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY; @@ -472,6 +479,7 @@ final class DisplayDeviceInfo { || modeId != other.modeId || renderFrameRate != other.renderFrameRate || defaultModeId != other.defaultModeId + || userPreferredModeId != other.userPreferredModeId || !Arrays.equals(supportedModes, other.supportedModes) || !Arrays.equals(supportedColorModes, other.supportedColorModes) || !Objects.equals(hdrCapabilities, other.hdrCapabilities) @@ -517,6 +525,7 @@ final class DisplayDeviceInfo { modeId = other.modeId; renderFrameRate = other.renderFrameRate; defaultModeId = other.defaultModeId; + userPreferredModeId = other.userPreferredModeId; supportedModes = other.supportedModes; colorMode = other.colorMode; supportedColorModes = other.supportedColorModes; @@ -559,6 +568,7 @@ final class DisplayDeviceInfo { sb.append(", modeId ").append(modeId); sb.append(", renderFrameRate ").append(renderFrameRate); sb.append(", defaultModeId ").append(defaultModeId); + sb.append(", userPreferredModeId ").append(userPreferredModeId); sb.append(", supportedModes ").append(Arrays.toString(supportedModes)); sb.append(", colorMode ").append(colorMode); sb.append(", supportedColorModes ").append(Arrays.toString(supportedColorModes)); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 46ef6c3bd3e4..ceb29e9e59c7 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -557,7 +557,7 @@ public final class DisplayManagerService extends SystemService { mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, new FoldSettingProvider(mContext, new SettingsWrapper()), mDisplayDeviceRepo, new LogicalDisplayListener(), mSyncRoot, mHandler, mFlags); - mDisplayModeDirector = new DisplayModeDirector(context, mHandler); + mDisplayModeDirector = new DisplayModeDirector(context, mHandler, mFlags); mBrightnessSynchronizer = new BrightnessSynchronizer(mContext); Resources resources = mContext.getResources(); mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger( diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 924b1b3c66ab..b32a207f60b8 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -640,6 +640,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { mInfo.modeId = mActiveModeId; mInfo.renderFrameRate = mActiveRenderFrameRate; mInfo.defaultModeId = getPreferredModeId(); + mInfo.userPreferredModeId = mUserPreferredModeId; mInfo.supportedModes = getDisplayModes(mSupportedModes); mInfo.colorMode = mActiveColorMode; mInfo.allmSupported = mAllmSupported; diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 0405ebe8b5d2..d4d104e862f0 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -470,6 +470,7 @@ final class LogicalDisplay { mBaseDisplayInfo.modeId = deviceInfo.modeId; mBaseDisplayInfo.renderFrameRate = deviceInfo.renderFrameRate; mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId; + mBaseDisplayInfo.userPreferredModeId = deviceInfo.userPreferredModeId; mBaseDisplayInfo.supportedModes = Arrays.copyOf( deviceInfo.supportedModes, deviceInfo.supportedModes.length); mBaseDisplayInfo.colorMode = deviceInfo.colorMode; diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 3f6bf1adfe48..ff768d64a7e1 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -47,6 +47,22 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1, Flags::enableAdaptiveToneImprovements1); + private final FlagState mDisplayResolutionRangeVotingState = new FlagState( + Flags.FLAG_ENABLE_DISPLAY_RESOLUTION_RANGE_VOTING, + Flags::enableDisplayResolutionRangeVoting); + + private final FlagState mUserPreferredModeVoteState = new FlagState( + Flags.FLAG_ENABLE_USER_PREFERRED_MODE_VOTE, + Flags::enableUserPreferredModeVote); + + private final FlagState mExternalDisplayLimitModeState = new FlagState( + Flags.FLAG_ENABLE_MODE_LIMIT_FOR_EXTERNAL_DISPLAY, + Flags::enableModeLimitForExternalDisplay); + + private final FlagState mDisplaysRefreshRatesSynchronizationState = new FlagState( + Flags.FLAG_ENABLE_DISPLAYS_REFRESH_RATES_SYNCHRONIZATION, + Flags::enableDisplaysRefreshRatesSynchronization); + /** Returns whether connected display management is enabled or not. */ public boolean isConnectedDisplayManagementEnabled() { return mConnectedDisplayManagementFlagState.isEnabled(); @@ -68,6 +84,33 @@ public class DisplayManagerFlags { return mAdaptiveToneImprovements1.isEnabled(); } + /** Returns whether resolution range voting feature is enabled or not. */ + public boolean isDisplayResolutionRangeVotingEnabled() { + return mDisplayResolutionRangeVotingState.isEnabled(); + } + + /** + * @return Whether user preferred mode is added as a vote in + * {@link com.android.server.display.mode.DisplayModeDirector} + */ + public boolean isUserPreferredModeVoteEnabled() { + return mUserPreferredModeVoteState.isEnabled(); + } + + /** + * @return Whether external display mode limitation is enabled. + */ + public boolean isExternalDisplayLimitModeEnabled() { + return mExternalDisplayLimitModeState.isEnabled(); + } + + /** + * @return Whether displays refresh rate synchronization is enabled. + */ + public boolean isDisplaysRefreshRatesSynchronizationEnabled() { + return mDisplaysRefreshRatesSynchronizationState.isEnabled(); + } + private static class FlagState { private final String mName; diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 4d8600448c33..a5b8cbbcdec4 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -5,7 +5,7 @@ package: "com.android.server.display.feature.flags" flag { name: "enable_connected_display_management" namespace: "display_manager" - description: "Feature flag for Connected Display managment" + description: "Feature flag for Connected Display management" bug: "280739508" is_fixed_read_only: true } @@ -34,3 +34,34 @@ flag { is_fixed_read_only: true } +flag { + name: "enable_display_resolution_range_voting" + namespace: "display_manager" + description: "Feature flag to enable voting for ranges of resolutions" + bug: "299297058" + is_fixed_read_only: true +} + +flag { + name: "enable_user_preferred_mode_vote" + namespace: "display_manager" + description: "Feature flag to use voting for UserPreferredMode for display" + bug: "297018612" + is_fixed_read_only: true +} + +flag { + name: "enable_mode_limit_for_external_display" + namespace: "display_manager" + description: "Feature limiting external display resolution and refresh rate" + bug: "242093547" + is_fixed_read_only: true +} + +flag { + name: "enable_displays_refresh_rates_synchronization" + namespace: "display_manager" + description: "Enables synchronization of refresh rates across displays" + bug: "294015845" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 2c2af3f7f435..71ea8cc30405 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -19,6 +19,9 @@ package com.android.server.display.mode; import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; import static android.hardware.display.DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE; import static android.os.PowerManager.BRIGHTNESS_INVALID_FLOAT; +import static android.view.Display.Mode.INVALID_MODE_ID; + +import static com.android.server.display.DisplayDeviceConfig.DEFAULT_LOW_REFRESH_RATE; import android.annotation.IntegerRes; import android.annotation.NonNull; @@ -71,6 +74,7 @@ import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.feature.DeviceConfigParameterProvider; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.utils.AmbientFilter; import com.android.server.display.utils.AmbientFilterFactory; import com.android.server.display.utils.DeviceConfigParsingUtils; @@ -84,9 +88,11 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Callable; import java.util.function.Function; import java.util.function.IntSupplier; @@ -96,6 +102,8 @@ import java.util.function.IntSupplier; * picked by the system based on system-wide and display-specific configuration. */ public class DisplayModeDirector { + public static final float SYNCHRONIZED_REFRESH_RATE_TARGET = DEFAULT_LOW_REFRESH_RATE; + public static final float SYNCHRONIZED_REFRESH_RATE_TOLERANCE = 1; private static final String TAG = "DisplayModeDirector"; private boolean mLoggingEnabled; @@ -151,12 +159,38 @@ public class DisplayModeDirector { @DisplayManager.SwitchingType private int mModeSwitchingType = DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS; - public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler) { - this(context, handler, new RealInjector(context)); + /** + * Whether resolution range voting feature is enabled. + */ + private final boolean mIsDisplayResolutionRangeVotingEnabled; + + /** + * Whether user preferred mode voting feature is enabled. + */ + private final boolean mIsUserPreferredModeVoteEnabled; + + /** + * Whether limit display mode feature is enabled. + */ + private final boolean mIsExternalDisplayLimitModeEnabled; + + private final boolean mIsDisplaysRefreshRatesSynchronizationEnabled; + + public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler, + @NonNull DisplayManagerFlags displayManagerFlags) { + this(context, handler, new RealInjector(context), displayManagerFlags); } public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler, - @NonNull Injector injector) { + @NonNull Injector injector, + @NonNull DisplayManagerFlags displayManagerFlags) { + mIsDisplayResolutionRangeVotingEnabled = displayManagerFlags + .isDisplayResolutionRangeVotingEnabled(); + mIsUserPreferredModeVoteEnabled = displayManagerFlags.isUserPreferredModeVoteEnabled(); + mIsExternalDisplayLimitModeEnabled = displayManagerFlags + .isExternalDisplayLimitModeEnabled(); + mIsDisplaysRefreshRatesSynchronizationEnabled = displayManagerFlags + .isDisplaysRefreshRatesSynchronizationEnabled(); mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; @@ -230,6 +264,8 @@ public class DisplayModeDirector { public float maxRenderFrameRate; public int width; public int height; + public int minWidth; + public int minHeight; public boolean disableRefreshRateSwitching; public float appRequestBaseModeRefreshRate; @@ -244,6 +280,8 @@ public class DisplayModeDirector { maxRenderFrameRate = Float.POSITIVE_INFINITY; width = Vote.INVALID_SIZE; height = Vote.INVALID_SIZE; + minWidth = 0; + minHeight = 0; disableRefreshRateSwitching = false; appRequestBaseModeRefreshRate = 0f; } @@ -256,6 +294,8 @@ public class DisplayModeDirector { + ", maxRenderFrameRate=" + maxRenderFrameRate + ", width=" + width + ", height=" + height + + ", minWidth=" + minWidth + + ", minHeight=" + minHeight + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate; } @@ -277,7 +317,6 @@ public class DisplayModeDirector { continue; } - // For physical refresh rates, just use the tightest bounds of all the votes. // The refresh rate cannot be lower than the minimal render frame rate. final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min, @@ -298,10 +337,18 @@ public class DisplayModeDirector { // For display size, disable refresh rate switching and base mode refresh rate use only // the first vote we come across (i.e. the highest priority vote that includes the // attribute). - if (summary.height == Vote.INVALID_SIZE && summary.width == Vote.INVALID_SIZE - && vote.height > 0 && vote.width > 0) { - summary.width = vote.width; - summary.height = vote.height; + if (vote.height > 0 && vote.width > 0) { + if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) { + summary.width = vote.width; + summary.height = vote.height; + summary.minWidth = vote.minWidth; + summary.minHeight = vote.minHeight; + } else if (mIsDisplayResolutionRangeVotingEnabled) { + summary.width = Math.min(summary.width, vote.width); + summary.height = Math.min(summary.height, vote.height); + summary.minWidth = Math.max(summary.minWidth, vote.minWidth); + summary.minHeight = Math.max(summary.minHeight, vote.minHeight); + } } if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) { summary.disableRefreshRateSwitching = true; @@ -413,6 +460,8 @@ public class DisplayModeDirector { || primarySummary.width == Vote.INVALID_SIZE) { primarySummary.width = defaultMode.getPhysicalWidth(); primarySummary.height = defaultMode.getPhysicalHeight(); + } else if (mIsDisplayResolutionRangeVotingEnabled) { + updateSummaryWithBestAllowedResolution(modes, primarySummary); } availableModes = filterModes(modes, primarySummary); @@ -654,6 +703,38 @@ public class DisplayModeDirector { return availableModes; } + private void updateSummaryWithBestAllowedResolution(final Display.Mode[] supportedModes, + VoteSummary outSummary) { + final int maxAllowedWidth = outSummary.width; + final int maxAllowedHeight = outSummary.height; + if (mLoggingEnabled) { + Slog.i(TAG, "updateSummaryWithBestAllowedResolution " + outSummary); + } + outSummary.width = Vote.INVALID_SIZE; + outSummary.height = Vote.INVALID_SIZE; + + int maxNumberOfPixels = 0; + for (Display.Mode mode : supportedModes) { + if (mode.getPhysicalWidth() > maxAllowedWidth + || mode.getPhysicalHeight() > maxAllowedHeight + || mode.getPhysicalWidth() < outSummary.minWidth + || mode.getPhysicalHeight() < outSummary.minHeight) { + continue; + } + + int numberOfPixels = mode.getPhysicalHeight() * mode.getPhysicalWidth(); + if (numberOfPixels > maxNumberOfPixels || (mode.getPhysicalWidth() == maxAllowedWidth + && mode.getPhysicalHeight() == maxAllowedHeight)) { + if (mLoggingEnabled) { + Slog.i(TAG, "updateSummaryWithBestAllowedResolution updated with " + mode); + } + maxNumberOfPixels = numberOfPixels; + outSummary.width = mode.getPhysicalWidth(); + outSummary.height = mode.getPhysicalHeight(); + } + } + } + /** * Gets the observer responsible for application display mode requests. */ @@ -1393,11 +1474,38 @@ public class DisplayModeDirector { private final Context mContext; private final Handler mHandler; private final VotesStorage mVotesStorage; + private int mExternalDisplayPeakWidth; + private int mExternalDisplayPeakHeight; + private int mExternalDisplayPeakRefreshRate; + private final boolean mRefreshRateSynchronizationEnabled; + private final Set<Integer> mExternalDisplaysConnected = new HashSet<>(); DisplayObserver(Context context, Handler handler, VotesStorage votesStorage) { mContext = context; mHandler = handler; mVotesStorage = votesStorage; + mExternalDisplayPeakRefreshRate = mContext.getResources().getInteger( + R.integer.config_externalDisplayPeakRefreshRate); + mExternalDisplayPeakWidth = mContext.getResources().getInteger( + R.integer.config_externalDisplayPeakWidth); + mExternalDisplayPeakHeight = mContext.getResources().getInteger( + R.integer.config_externalDisplayPeakHeight); + mRefreshRateSynchronizationEnabled = mContext.getResources().getBoolean( + R.bool.config_refreshRateSynchronizationEnabled); + } + + private boolean isExternalDisplayLimitModeEnabled() { + return mExternalDisplayPeakWidth > 0 + && mExternalDisplayPeakHeight > 0 + && mExternalDisplayPeakRefreshRate > 0 + && mIsExternalDisplayLimitModeEnabled + && mIsDisplayResolutionRangeVotingEnabled + && mIsUserPreferredModeVoteEnabled; + } + + private boolean isRefreshRateSynchronizationEnabled() { + return mRefreshRateSynchronizationEnabled + && mIsDisplaysRefreshRatesSynchronizationEnabled; } public void observe() { @@ -1428,6 +1536,9 @@ public class DisplayModeDirector { DisplayInfo displayInfo = getDisplayInfo(displayId); updateDisplayModes(displayId, displayInfo); updateLayoutLimitedFrameRate(displayId, displayInfo); + updateUserSettingDisplayPreferredSize(displayInfo); + updateDisplaysPeakRefreshRateAndResolution(displayInfo); + addDisplaysSynchronizedPeakRefreshRate(displayInfo); } @Override @@ -1437,6 +1548,9 @@ public class DisplayModeDirector { mDefaultModeByDisplay.remove(displayId); } updateLayoutLimitedFrameRate(displayId, null); + removeUserSettingDisplayPreferredSize(displayId); + removeDisplaysPeakRefreshRateAndResolution(displayId); + removeDisplaysSynchronizedPeakRefreshRate(displayId); } @Override @@ -1444,6 +1558,7 @@ public class DisplayModeDirector { DisplayInfo displayInfo = getDisplayInfo(displayId); updateDisplayModes(displayId, displayInfo); updateLayoutLimitedFrameRate(displayId, displayInfo); + updateUserSettingDisplayPreferredSize(displayInfo); } @Nullable @@ -1460,6 +1575,111 @@ public class DisplayModeDirector { mVotesStorage.updateVote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote); } + private void removeUserSettingDisplayPreferredSize(int displayId) { + if (!mIsUserPreferredModeVoteEnabled) { + return; + } + mVotesStorage.updateVote(displayId, Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE, + null); + } + + private void updateUserSettingDisplayPreferredSize(@Nullable DisplayInfo info) { + if (info == null || !mIsUserPreferredModeVoteEnabled) { + return; + } + + var preferredMode = findDisplayPreferredMode(info); + if (preferredMode == null) { + removeUserSettingDisplayPreferredSize(info.displayId); + return; + } + + mVotesStorage.updateVote(info.displayId, + Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE, + Vote.forSize(/* width */ preferredMode.getPhysicalWidth(), + /* height */ preferredMode.getPhysicalHeight())); + } + + @Nullable + private Display.Mode findDisplayPreferredMode(@NonNull DisplayInfo info) { + if (info.userPreferredModeId == INVALID_MODE_ID) { + return null; + } + for (var mode : info.supportedModes) { + if (mode.getModeId() == info.userPreferredModeId) { + return mode; + } + } + return null; + } + + private void removeDisplaysPeakRefreshRateAndResolution(int displayId) { + if (!isExternalDisplayLimitModeEnabled()) { + return; + } + + mVotesStorage.updateVote(displayId, + Vote.PRIORITY_LIMIT_MODE, null); + } + + private void updateDisplaysPeakRefreshRateAndResolution(@Nullable final DisplayInfo info) { + // Only consider external display, only in case the refresh rate and resolution limits + // are non-zero. + if (info == null || info.type != Display.TYPE_EXTERNAL + || !isExternalDisplayLimitModeEnabled()) { + return; + } + + mVotesStorage.updateVote(info.displayId, + Vote.PRIORITY_LIMIT_MODE, + Vote.forSizeAndPhysicalRefreshRatesRange( + /* minWidth */ 0, /* minHeight */ 0, + mExternalDisplayPeakWidth, + mExternalDisplayPeakHeight, + /* minPhysicalRefreshRate */ 0, + mExternalDisplayPeakRefreshRate)); + } + + /** + * Sets 60Hz target refresh rate as the vote with + * {@link Vote#PRIORITY_SYNCHRONIZED_REFRESH_RATE} priority. + */ + private void addDisplaysSynchronizedPeakRefreshRate(@Nullable final DisplayInfo info) { + if (info == null || info.type != Display.TYPE_EXTERNAL + || !isRefreshRateSynchronizationEnabled()) { + return; + } + synchronized (mLock) { + mExternalDisplaysConnected.add(info.displayId); + if (mExternalDisplaysConnected.size() != 1) { + return; + } + } + // set minRefreshRate as the max refresh rate. + mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE, + Vote.forPhysicalRefreshRates( + SYNCHRONIZED_REFRESH_RATE_TARGET + - SYNCHRONIZED_REFRESH_RATE_TOLERANCE, + SYNCHRONIZED_REFRESH_RATE_TARGET + + SYNCHRONIZED_REFRESH_RATE_TOLERANCE)); + } + + private void removeDisplaysSynchronizedPeakRefreshRate(final int displayId) { + if (!isRefreshRateSynchronizationEnabled()) { + return; + } + synchronized (mLock) { + if (!mExternalDisplaysConnected.contains(displayId)) { + return; + } + mExternalDisplaysConnected.remove(displayId); + if (mExternalDisplaysConnected.size() != 0) { + return; + } + } + mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE, null); + } + private void updateDisplayModes(int displayId, @Nullable DisplayInfo info) { if (info == null) { return; diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java index a42d8f257ddf..b6a6069b5a63 100644 --- a/services/core/java/com/android/server/display/mode/Vote.java +++ b/services/core/java/com/android/server/display/mode/Vote.java @@ -18,6 +18,8 @@ package com.android.server.display.mode; import android.view.SurfaceControl; +import java.util.Objects; + final class Vote { // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest // priority vote, it's overridden by all other considerations. It acts to set a default @@ -36,12 +38,15 @@ final class Vote { // It votes [minRefreshRate, Float.POSITIVE_INFINITY] static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3; + // User setting preferred display resolution. + static final int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4; + // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render // frame rate in certain cases, mostly to preserve power. // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate]. - static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4; + static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5; // We split the app request into different priorities in case we can satisfy one desire // without the other. @@ -67,40 +72,47 @@ final class Vote { // The preferred refresh rate is set on the main surface of the app outside of // DisplayModeDirector. // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded - static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5; - static final int PRIORITY_APP_REQUEST_SIZE = 6; + static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6; + + static final int PRIORITY_APP_REQUEST_SIZE = 7; // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the // rest of low priority voters. It votes [0, max(PEAK, MIN)] - static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7; + static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8; + + // Restrict all displays to 60Hz when external display is connected. It votes [59Hz, 61Hz]. + static final int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9; + + // Restrict displays max available resolution and refresh rates. It votes [0, LIMIT] + static final int PRIORITY_LIMIT_MODE = 10; // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh // rate to max value (same as for PRIORITY_UDFPS) on lock screen - static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8; + static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11; // For concurrent displays we want to limit refresh rate on all displays - static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9; + static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12; // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if // Settings.Global.LOW_POWER_MODE is on. - static final int PRIORITY_LOW_POWER_MODE = 10; + static final int PRIORITY_LOW_POWER_MODE = 13; // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the // higher priority voters' result is a range, it will fix the rate to a single choice. // It's used to avoid refresh rate switches in certain conditions which may result in the // user seeing the display flickering when the switches occur. - static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11; + static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14; // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL. - static final int PRIORITY_SKIN_TEMPERATURE = 12; + static final int PRIORITY_SKIN_TEMPERATURE = 15; // The proximity sensor needs the refresh rate to be locked in order to function, so this is // set to a high priority. - static final int PRIORITY_PROXIMITY = 13; + static final int PRIORITY_PROXIMITY = 16; // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order // to function, so this needs to be the highest priority of all votes. - static final int PRIORITY_UDFPS = 14; + static final int PRIORITY_UDFPS = 17; // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString. @@ -127,6 +139,14 @@ final class Vote { */ public final int height; /** + * Min requested width of the display in pixels, or 0; + */ + public final int minWidth; + /** + * Min requested height of the display in pixels, or 0; + */ + public final int minHeight; + /** * Information about the refresh rate frame rate ranges DM would like to set the display to. */ public final SurfaceControl.RefreshRateRanges refreshRateRanges; @@ -144,42 +164,82 @@ final class Vote { public final float appRequestBaseModeRefreshRate; static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) { - return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0, - Float.POSITIVE_INFINITY, - minRefreshRate == maxRefreshRate, 0f); + return new Vote(/* minWidth= */ 0, /* minHeight= */ 0, + /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE, + /* minPhysicalRefreshRate= */ minRefreshRate, + /* maxPhysicalRefreshRate= */ maxRefreshRate, + /* minRenderFrameRate= */ 0, + /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY, + /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate, + /* baseModeRefreshRate= */ 0f); } static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) { - return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate, + return new Vote(/* minWidth= */ 0, /* minHeight= */ 0, + /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE, + /* minPhysicalRefreshRate= */ 0, + /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY, + minFrameRate, maxFrameRate, - false, 0f); + /* disableRefreshRateSwitching= */ false, + /* baseModeRefreshRate= */ 0f); } static Vote forSize(int width, int height) { - return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY, - false, - 0f); + return new Vote(/* minWidth= */ width, /* minHeight= */ height, + width, height, + /* minPhysicalRefreshRate= */ 0, + /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY, + /* minRenderFrameRate= */ 0, + /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY, + /* disableRefreshRateSwitching= */ false, + /* baseModeRefreshRate= */ 0f); + } + + static Vote forSizeAndPhysicalRefreshRatesRange(int minWidth, int minHeight, + int width, int height, float minRefreshRate, float maxRefreshRate) { + return new Vote(minWidth, minHeight, + width, height, + minRefreshRate, + maxRefreshRate, + /* minRenderFrameRate= */ 0, + /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY, + /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate, + /* baseModeRefreshRate= */ 0f); } static Vote forDisableRefreshRateSwitching() { - return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0, - Float.POSITIVE_INFINITY, true, - 0f); + return new Vote(/* minWidth= */ 0, /* minHeight= */ 0, + /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE, + /* minPhysicalRefreshRate= */ 0, + /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY, + /* minRenderFrameRate= */ 0, + /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY, + /* disableRefreshRateSwitching= */ true, + /* baseModeRefreshRate= */ 0f); } static Vote forBaseModeRefreshRate(float baseModeRefreshRate) { - return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0, - Float.POSITIVE_INFINITY, false, - baseModeRefreshRate); + return new Vote(/* minWidth= */ 0, /* minHeight= */ 0, + /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE, + /* minPhysicalRefreshRate= */ 0, + /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY, + /* minRenderFrameRate= */ 0, + /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY, + /* disableRefreshRateSwitching= */ false, + /* baseModeRefreshRate= */ baseModeRefreshRate); } - private Vote(int width, int height, + private Vote(int minWidth, int minHeight, + int width, int height, float minPhysicalRefreshRate, float maxPhysicalRefreshRate, float minRenderFrameRate, float maxRenderFrameRate, boolean disableRefreshRateSwitching, float baseModeRefreshRate) { + this.minWidth = minWidth; + this.minHeight = minHeight; this.width = width; this.height = height; this.refreshRateRanges = new SurfaceControl.RefreshRateRanges( @@ -215,6 +275,12 @@ final class Vote { return "PRIORITY_UDFPS"; case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE: return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE"; + case PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE: + return "PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE"; + case PRIORITY_LIMIT_MODE: + return "PRIORITY_LIMIT_MODE"; + case PRIORITY_SYNCHRONIZED_REFRESH_RATE: + return "PRIORITY_SYNCHRONIZED_REFRESH_RATE"; case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE: return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE"; case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE: @@ -229,9 +295,29 @@ final class Vote { @Override public String toString() { return "Vote: {" - + "width: " + width + ", height: " + height + + "minWidth: " + minWidth + ", minHeight: " + minHeight + + ", width: " + width + ", height: " + height + ", refreshRateRanges: " + refreshRateRanges + ", disableRefreshRateSwitching: " + disableRefreshRateSwitching + ", appRequestBaseModeRefreshRate: " + appRequestBaseModeRefreshRate + "}"; } + + @Override + public int hashCode() { + return Objects.hash(minWidth, minHeight, width, height, refreshRateRanges, + disableRefreshRateSwitching, appRequestBaseModeRefreshRate); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Vote)) return false; + final var vote = (Vote) o; + return minWidth == vote.minWidth && minHeight == vote.minHeight + && width == vote.width && height == vote.height + && disableRefreshRateSwitching == vote.disableRefreshRateSwitching + && Float.compare(vote.appRequestBaseModeRefreshRate, + appRequestBaseModeRefreshRate) == 0 + && refreshRateRanges.equals(vote.refreshRateRanges); + } } diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java index bdd2ab7d63b4..49c587aa5596 100644 --- a/services/core/java/com/android/server/display/mode/VotesStorage.java +++ b/services/core/java/com/android/server/display/mode/VotesStorage.java @@ -31,7 +31,8 @@ class VotesStorage { private static final String TAG = "VotesStorage"; // Special ID used to indicate that given vote is to be applied globally, rather than to a // specific display. - private static final int GLOBAL_ID = -1; + @VisibleForTesting + static final int GLOBAL_ID = -1; private boolean mLoggingEnabled; @@ -91,6 +92,7 @@ class VotesStorage { + ", vote=" + vote); return; } + boolean changed = false; SparseArray<Vote> votes; synchronized (mStorageLock) { if (mVotesByDisplay.contains(displayId)) { @@ -99,10 +101,13 @@ class VotesStorage { votes = new SparseArray<>(); mVotesByDisplay.put(displayId, votes); } - if (vote != null) { + var currentVote = votes.get(priority); + if (vote != null && !vote.equals(currentVote)) { votes.put(priority, vote); - } else { + changed = true; + } else if (vote == null && currentVote != null) { votes.remove(priority); + changed = true; } } Trace.traceCounter(Trace.TRACE_TAG_POWER, @@ -111,7 +116,9 @@ class VotesStorage { if (mLoggingEnabled) { Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes); } - mListener.onChanged(); + if (changed) { + mListener.onChanged(); + } } /** dump class values, for debugging */ diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index fbad369ab19e..b8c18e070397 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -91,6 +91,7 @@ import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.TestUtils; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver; import com.android.server.display.mode.DisplayModeDirector.DesiredDisplayModeSpecs; import com.android.server.sensors.SensorManagerInternal; @@ -110,7 +111,9 @@ import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -121,10 +124,114 @@ import junitparams.Parameters; @SmallTest @RunWith(JUnitParamsRunner.class) public class DisplayModeDirectorTest { - // The tolerance within which we consider something approximately equals. + public static Collection<Object[]> getAppRequestedSizeTestCases() { + var appRequestedSizeTestCases = Arrays.asList(new Object[][] { + {DEFAULT_MODE_75.getModeId(), Float.POSITIVE_INFINITY, + DEFAULT_MODE_75.getRefreshRate(), Map.of()}, + {APP_MODE_HIGH_90.getModeId(), Float.POSITIVE_INFINITY, + APP_MODE_HIGH_90.getRefreshRate(), + Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), + APP_MODE_HIGH_90.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()))}, + {LIMIT_MODE_70.getModeId(), Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), + APP_MODE_HIGH_90.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight()))}, + {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(), + LIMIT_MODE_70.getRefreshRate(), + Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_65.getPhysicalWidth(), + APP_MODE_65.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight()))}, + {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(), + LIMIT_MODE_70.getRefreshRate(), + Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_65.getPhysicalWidth(), + APP_MODE_65.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSizeAndPhysicalRefreshRatesRange( + 0, 0, + LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight(), + 0, Float.POSITIVE_INFINITY)), false}, + {APP_MODE_65.getModeId(), APP_MODE_65.getRefreshRate(), + APP_MODE_65.getRefreshRate(), + Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_65.getPhysicalWidth(), + APP_MODE_65.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSizeAndPhysicalRefreshRatesRange( + 0, 0, + LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight(), + 0, Float.POSITIVE_INFINITY)), true}}); + + final var res = new ArrayList<Object[]>(appRequestedSizeTestCases.size() * 2); + + // Add additional argument for displayResolutionRangeVotingEnabled=false if not present. + for (var testCaseArrayArgs : appRequestedSizeTestCases) { + if (testCaseArrayArgs.length == 4) { + var testCaseListArgs = new ArrayList<>(Arrays.asList(testCaseArrayArgs)); + testCaseListArgs.add(/* displayResolutionRangeVotingEnabled */ false); + res.add(testCaseListArgs.toArray()); + } else { + res.add(testCaseArrayArgs); + } + } + + // Add additional argument for displayResolutionRangeVotingEnabled=true if not present. + for (var testCaseArrayArgs : appRequestedSizeTestCases) { + if (testCaseArrayArgs.length == 4) { + var testCaseListArgs = new ArrayList<>(Arrays.asList(testCaseArrayArgs)); + testCaseListArgs.add(/* displayResolutionRangeVotingEnabled */ true); + res.add(testCaseListArgs.toArray()); + } + } + + return res; + } + private static final String TAG = "DisplayModeDirectorTest"; private static final boolean DEBUG = false; private static final float FLOAT_TOLERANCE = 0.01f; + + private static final Display.Mode APP_MODE_65 = new Display.Mode( + /*modeId=*/65, /*width=*/1900, /*height=*/1900, 65); + private static final Display.Mode LIMIT_MODE_70 = new Display.Mode( + /*modeId=*/70, /*width=*/2000, /*height=*/2000, 70); + private static final Display.Mode DEFAULT_MODE_75 = new Display.Mode( + /*modeId=*/75, /*width=*/2500, /*height=*/2500, 75); + private static final Display.Mode APP_MODE_HIGH_90 = new Display.Mode( + /*modeId=*/90, /*width=*/3000, /*height=*/3000, 90); + private static final Display.Mode[] TEST_MODES = new Display.Mode[] { + new Display.Mode( + /*modeId=*/60, /*width=*/1900, /*height=*/1900, 60), + APP_MODE_65, + LIMIT_MODE_70, + DEFAULT_MODE_75, + APP_MODE_HIGH_90 + }; + private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY; private static final int MODE_ID = 1; private static final float TRANSITION_POINT = 0.763f; @@ -142,6 +249,8 @@ public class DisplayModeDirectorTest { public SensorManagerInternal mSensorManagerInternalMock; @Mock public DisplayManagerInternal mDisplayManagerInternalMock; + @Mock + private DisplayManagerFlags mDisplayManagerFlags; @Before public void setUp() throws Exception { @@ -177,7 +286,7 @@ public class DisplayModeDirectorTest { private DisplayModeDirector createDirectorFromModeArray(Display.Mode[] modes, Display.Mode defaultMode) { DisplayModeDirector director = - new DisplayModeDirector(mContext, mHandler, mInjector); + new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags); director.setLoggingEnabled(true); SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>(); supportedModesByDisplay.put(DISPLAY_ID, modes); @@ -219,9 +328,8 @@ public class DisplayModeDirectorTest { // should take precedence over lower priority votes. { int minFps = 60; - int maxFps = 90; - director = createDirectorFromFpsRange(60, 90); - assertTrue(2 * numPriorities < maxFps - minFps + 1); + int maxFps = minFps + 2 * numPriorities; + director = createDirectorFromFpsRange(minFps, maxFps); SparseArray<Vote> votes = new SparseArray<>(); SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>(); votesByDisplay.put(DISPLAY_ID, votes); @@ -472,6 +580,7 @@ public class DisplayModeDirectorTest { assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90); } + /** Resolution range voting disabled */ @Test public void testAppRequestRefreshRateRange() { // Confirm that the app request range doesn't include flicker or min refresh rate settings, @@ -530,6 +639,33 @@ public class DisplayModeDirectorTest { assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f); } + /** Tests for app requested size */ + @Parameters(method = "getAppRequestedSizeTestCases") + @Test + public void testAppRequestedSize(final int expectedBaseModeId, + final float expectedPhysicalRefreshRate, + final float expectedAppRequestedRefreshRate, + final Map<Integer, Vote> votesWithPriorities, + final boolean displayResolutionRangeVotingEnabled) { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()) + .thenReturn(displayResolutionRangeVotingEnabled); + DisplayModeDirector director = createDirectorFromModeArray(TEST_MODES, DEFAULT_MODE_75); + + SparseArray<Vote> votes = new SparseArray<>(); + votesWithPriorities.forEach(votes::put); + + SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>(); + votesByDisplay.put(DISPLAY_ID, votes); + director.injectVotesByDisplay(votesByDisplay); + + var desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID); + assertThat(desiredSpecs.baseModeId).isEqualTo(expectedBaseModeId); + assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0); + assertThat(desiredSpecs.primary.physical.max).isAtLeast(expectedPhysicalRefreshRate); + assertThat(desiredSpecs.appRequest.physical.min).isAtMost(0); + assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(expectedAppRequestedRefreshRate); + } + void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps, float peakFps, float defaultFps, RefreshRateRanges primary, RefreshRateRanges appRequest) { @@ -843,7 +979,7 @@ public class DisplayModeDirectorTest { @Test public void testStaleAppRequestSize() { DisplayModeDirector director = - new DisplayModeDirector(mContext, mHandler, mInjector); + new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags); Display.Mode[] modes = new Display.Mode[] { new Display.Mode(1, 1280, 720, 60), }; diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java new file mode 100644 index 000000000000..ff91d34470d4 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.mode; + + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.Mode.INVALID_MODE_ID; + + +import static com.android.server.display.mode.DisplayModeDirector.SYNCHRONIZED_REFRESH_RATE_TOLERANCE; +import static com.android.server.display.mode.Vote.PRIORITY_LIMIT_MODE; +import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE; +import static com.android.server.display.mode.Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE; +import static com.android.server.display.mode.VotesStorage.GLOBAL_ID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.Looper; +import android.provider.DeviceConfigInterface; +import android.view.Display; +import android.view.DisplayInfo; + +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.server.display.feature.DisplayManagerFlags; +import com.android.server.sensors.SensorManagerInternal; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import junitparams.JUnitParamsRunner; + + +@SmallTest +@RunWith(JUnitParamsRunner.class) +public class DisplayObserverTest { + private static final int EXTERNAL_DISPLAY = 1; + private static final int MAX_WIDTH = 1920; + private static final int MAX_HEIGHT = 1080; + private static final int MAX_REFRESH_RATE = 60; + + private final Display.Mode[] mInternalDisplayModes = new Display.Mode[] { + new Display.Mode(/*modeId=*/ 0, MAX_WIDTH / 2, MAX_HEIGHT / 2, + (float) MAX_REFRESH_RATE / 2), + new Display.Mode(/*modeId=*/ 1, MAX_WIDTH / 2, MAX_HEIGHT / 2, + MAX_REFRESH_RATE), + new Display.Mode(/*modeId=*/ 2, MAX_WIDTH / 2, MAX_HEIGHT / 2, + MAX_REFRESH_RATE * 2), + new Display.Mode(/*modeId=*/ 3, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE * 2), + new Display.Mode(/*modeId=*/ 4, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE), + new Display.Mode(/*modeId=*/ 5, MAX_WIDTH * 2, MAX_HEIGHT * 2, + MAX_REFRESH_RATE), + new Display.Mode(/*modeId=*/ 6, MAX_WIDTH / 2, MAX_HEIGHT / 2, + MAX_REFRESH_RATE * 3), + }; + + private final Display.Mode[] mExternalDisplayModes = new Display.Mode[] { + new Display.Mode(/*modeId=*/ 0, MAX_WIDTH / 2, MAX_HEIGHT / 2, + (float) MAX_REFRESH_RATE / 2), + new Display.Mode(/*modeId=*/ 1, MAX_WIDTH / 2, MAX_HEIGHT / 2, + MAX_REFRESH_RATE), + new Display.Mode(/*modeId=*/ 2, MAX_WIDTH / 2, MAX_HEIGHT / 2, + MAX_REFRESH_RATE * 2), + new Display.Mode(/*modeId=*/ 3, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE * 2), + new Display.Mode(/*modeId=*/ 4, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE), + new Display.Mode(/*modeId=*/ 5, MAX_WIDTH * 2, MAX_HEIGHT * 2, + MAX_REFRESH_RATE), + }; + + private DisplayModeDirector mDmd; + private Context mContext; + private DisplayModeDirector.Injector mInjector; + private Handler mHandler; + private DisplayManager.DisplayListener mObserver; + private Resources mResources; + @Mock + private DisplayManagerFlags mDisplayManagerFlags; + private int mExternalDisplayUserPreferredModeId = INVALID_MODE_ID; + private int mInternalDisplayUserPreferredModeId = INVALID_MODE_ID; + private Display mDefaultDisplay; + private Display mExternalDisplay; + + /** Setup tests. */ + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mHandler = new Handler(Looper.getMainLooper()); + mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + mResources = mock(Resources.class); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(0); + when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(0); + when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(0); + when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled)) + .thenReturn(false); + + // Necessary configs to initialize DisplayModeDirector + when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) + .thenReturn(new int[]{5}); + when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) + .thenReturn(new int[]{10}); + when(mResources.getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(new int[]{250}); + when(mResources.getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(new int[]{7000}); + } + + /** No vote for user preferred mode */ + @Test + public void testExternalDisplay_notVotedUserPreferredMode() { + var preferredMode = mExternalDisplayModes[5]; + mExternalDisplayUserPreferredModeId = preferredMode.getModeId(); + + init(); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + + // Testing that the vote is not added when display is added because feature is disabled + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + + // Testing that the vote is not present after display is removed + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + + // Testing that the vote is not added when display is changed because feature is disabled + mObserver.onDisplayChanged(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + } + + /** Vote for user preferred mode */ + @Test + public void testExternalDisplay_voteUserPreferredMode() { + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + var preferredMode = mExternalDisplayModes[5]; + mExternalDisplayUserPreferredModeId = preferredMode.getModeId(); + var expectedVote = Vote.forSize( + preferredMode.getPhysicalWidth(), + preferredMode.getPhysicalHeight()); + init(); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(expectedVote); + + mExternalDisplayUserPreferredModeId = INVALID_MODE_ID; + mObserver.onDisplayChanged(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + + preferredMode = mExternalDisplayModes[4]; + mExternalDisplayUserPreferredModeId = preferredMode.getModeId(); + expectedVote = Vote.forSize( + preferredMode.getPhysicalWidth(), + preferredMode.getPhysicalHeight()); + mObserver.onDisplayChanged(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(expectedVote); + + // Testing that the vote is removed. + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + } + + /** External display: Do not apply limit to user preferred mode */ + @Test + public void testExternalDisplay_doNotApplyLimitToUserPreferredMode() { + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(MAX_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(MAX_WIDTH); + when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(MAX_HEIGHT); + + var preferredMode = mExternalDisplayModes[5]; + mExternalDisplayUserPreferredModeId = preferredMode.getModeId(); + var expectedResolutionVote = Vote.forSize(preferredMode.getPhysicalWidth(), + preferredMode.getPhysicalHeight()); + init(); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(expectedResolutionVote); + + // Testing that the vote is removed. + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + } + + /** Default display: Do not apply limit to user preferred mode */ + @Test + public void testDefaultDisplayAdded_notAppliedLimitToUserPreferredMode() { + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(MAX_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(MAX_WIDTH); + when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(MAX_HEIGHT); + var preferredMode = mInternalDisplayModes[5]; + mInternalDisplayUserPreferredModeId = preferredMode.getModeId(); + var expectedResolutionVote = Vote.forSize(preferredMode.getPhysicalWidth(), + preferredMode.getPhysicalHeight()); + init(); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + mObserver.onDisplayAdded(DEFAULT_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(expectedResolutionVote); + mObserver.onDisplayRemoved(DEFAULT_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE)) + .isEqualTo(null); + } + + /** Default display added, no mode limit set */ + @Test + public void testDefaultDisplayAdded() { + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(MAX_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(MAX_WIDTH); + when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(MAX_HEIGHT); + init(); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + mObserver.onDisplayAdded(DEFAULT_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + } + + /** External display added, apply resolution refresh rate limit */ + @Test + public void testExternalDisplayAdded_applyResolutionRefreshRateLimit() { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(MAX_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(MAX_WIDTH); + when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(MAX_HEIGHT); + init(); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo( + Vote.forSizeAndPhysicalRefreshRatesRange(0, 0, + MAX_WIDTH, MAX_HEIGHT, + /*minPhysicalRefreshRate=*/ 0, MAX_REFRESH_RATE)); + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + } + + /** External display added, disabled resolution refresh rate limit. */ + @Test + public void testExternalDisplayAdded_disabledResolutionRefreshRateLimit() { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + init(); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + mObserver.onDisplayChanged(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null); + } + + /** External display added, applied refresh rates synchronization */ + @Test + public void testExternalDisplayAdded_appliedRefreshRatesSynchronization() { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(true); + when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled)) + .thenReturn(true); + init(); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo( + Vote.forPhysicalRefreshRates( + MAX_REFRESH_RATE - SYNCHRONIZED_REFRESH_RATE_TOLERANCE, + MAX_REFRESH_RATE + SYNCHRONIZED_REFRESH_RATE_TOLERANCE)); + + // Remove external display and check that sync vote is no longer present. + mObserver.onDisplayRemoved(EXTERNAL_DISPLAY); + + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + } + + /** External display added, disabled feature refresh rates synchronization */ + @Test + public void testExternalDisplayAdded_disabledFeatureRefreshRatesSynchronization() { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(false); + when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled)) + .thenReturn(true); + init(); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + } + + /** External display not applied refresh rates synchronization, because + * config_refreshRateSynchronizationEnabled is false. */ + @Test + public void testExternalDisplay_notAppliedRefreshRatesSynchronization() { + when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true); + when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(true); + init(); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + mObserver.onDisplayAdded(EXTERNAL_DISPLAY); + assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null); + } + + private void init() { + mInjector = mock(DisplayModeDirector.Injector.class); + doAnswer(invocation -> { + assertThat(mObserver).isNull(); + mObserver = invocation.getArgument(0); + return null; + }).when(mInjector).registerDisplayListener(any(), any()); + + doAnswer(c -> { + DisplayInfo info = c.getArgument(1); + info.type = Display.TYPE_INTERNAL; + info.displayId = DEFAULT_DISPLAY; + info.defaultModeId = 0; + info.supportedModes = mInternalDisplayModes; + info.userPreferredModeId = mInternalDisplayUserPreferredModeId; + return true; + }).when(mInjector).getDisplayInfo(eq(DEFAULT_DISPLAY), /*displayInfo=*/ any()); + + doAnswer(c -> { + DisplayInfo info = c.getArgument(1); + info.type = Display.TYPE_EXTERNAL; + info.displayId = EXTERNAL_DISPLAY; + info.defaultModeId = 0; + info.supportedModes = mExternalDisplayModes; + info.userPreferredModeId = mExternalDisplayUserPreferredModeId; + return true; + }).when(mInjector).getDisplayInfo(eq(EXTERNAL_DISPLAY), /*displayInfo=*/ any()); + + doAnswer(c -> mock(SensorManagerInternal.class)).when(mInjector).getSensorManagerInternal(); + doAnswer(c -> mock(DeviceConfigInterface.class)).when(mInjector).getDeviceConfig(); + + mDefaultDisplay = mock(Display.class); + when(mDefaultDisplay.getDisplayId()).thenReturn(DEFAULT_DISPLAY); + doAnswer(c -> mInjector.getDisplayInfo(DEFAULT_DISPLAY, c.getArgument(0))) + .when(mDefaultDisplay).getDisplayInfo(/*displayInfo=*/ any()); + + mExternalDisplay = mock(Display.class); + when(mExternalDisplay.getDisplayId()).thenReturn(EXTERNAL_DISPLAY); + doAnswer(c -> mInjector.getDisplayInfo(EXTERNAL_DISPLAY, c.getArgument(0))) + .when(mExternalDisplay).getDisplayInfo(/*displayInfo=*/ any()); + + when(mInjector.getDisplays()).thenReturn(new Display[] {mDefaultDisplay, mExternalDisplay}); + + mDmd = new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags); + mDmd.start(null); + assertThat(mObserver).isNotNull(); + } + + @Nullable + private Vote getVote(final int displayId, final int priority) { + return mDmd.getVote(displayId, priority); + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java index 287fdd5c344b..50e239218fa0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java @@ -19,6 +19,7 @@ package com.android.server.display.mode; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.util.SparseArray; @@ -72,6 +73,18 @@ public class VotesStorageTest { verify(mVotesListener).onChanged(); } + /** Verifies that adding the same vote twice results in a single call to onChanged */ + @Test + public void notifiesVoteListenerCalledOnceIfVoteUpdatedTwice() { + // WHEN updateVote is called + mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE); + mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE); + mVotesStorage.updateVote(DISPLAY_ID, PRIORITY_OTHER, VOTE_OTHER); + mVotesStorage.updateVote(DISPLAY_ID, PRIORITY_OTHER, VOTE); + // THEN listener is notified, but only when vote changes. + verify(mVotesListener, times(3)).onChanged(); + } + @Test public void addsAnotherVoteToStorageWithDifferentPriority() { // GIVEN vote storage with one vote |