Files
packages_apps_Settings/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java
Matthew DeVore ded1d1c0d6 CD settings: minimal DisplayDevice metadata class
This saves tests from interacting with DisplayManager completely,
simplifies the Injector API, and makes mocking that API easier. This
addresses the first difficulty I had in converting our tests to
Robolectric.

Bug: b/399743032
Flag: com.android.settings.flags.display_topology_pane_in_display_list
Test: verify disabled and enabled displays appear correctly in display list fragment
Test: atest ExternalDisplayPreferenceFragmentTest.java
Test: atest ExternalDisplayUpdaterTest.java
Test: atest ResolutionPreferenceFragmentTest.java
Change-Id: Id43d2f108f3e85e6596eb2271b1de6b1afd2338f
2025-03-03 10:37:48 -06:00

358 lines
14 KiB
Java

/*
* Copyright (C) 2024 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.settings.connecteddevice.display;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.Display.Mode;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.internal.util.ToBooleanFunction;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragmentBase;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase {
private static final String TAG = "ResolutionPreference";
static final int DEFAULT_LOW_REFRESH_RATE = 60;
static final String MORE_OPTIONS_KEY = "more_options";
static final String TOP_OPTIONS_KEY = "top_options";
static final int MORE_OPTIONS_TITLE_RESOURCE =
R.string.external_display_more_options_title;
static final int EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE =
R.xml.external_display_resolution_settings;
static final String DISPLAY_MODE_LIMIT_OVERRIDE_PROP = "persist.sys.com.android.server.display"
+ ".feature.flags.enable_mode_limit_for_external_display-override";
@Nullable
private Injector mInjector;
@Nullable
private PreferenceCategory mTopOptionsPreference;
@Nullable
private PreferenceCategory mMoreOptionsPreference;
private boolean mStarted;
private final HashSet<String> mResolutionPreferences = new HashSet<>();
private int mExternalDisplayPeakWidth;
private int mExternalDisplayPeakHeight;
private int mExternalDisplayPeakRefreshRate;
private boolean mRefreshRateSynchronizationEnabled;
private boolean mMoreOptionsExpanded;
private final Runnable mUpdateRunnable = this::update;
private final DisplayListener mListener = new DisplayListener() {
@Override
public void update(int displayId) {
scheduleUpdate();
}
};
@Override
public int getMetricsCategory() {
return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
}
@Override
public int getHelpResource() {
return EXTERNAL_DISPLAY_HELP_URL;
}
@Override
public void onCreateCallback(@Nullable Bundle icicle) {
if (mInjector == null) {
mInjector = new Injector(getPrefContext());
}
addPreferencesFromResource(EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE);
updateDisplayModeLimits(mInjector.getContext());
}
@Override
public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) {
View view = getView();
TextView emptyView = null;
if (view != null) {
emptyView = (TextView) view.findViewById(android.R.id.empty);
}
if (emptyView != null) {
emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE);
setEmptyView(emptyView);
}
}
@Override
public void onStartCallback() {
mStarted = true;
if (mInjector == null) {
return;
}
mInjector.registerDisplayListener(mListener);
scheduleUpdate();
}
@Override
public void onStopCallback() {
mStarted = false;
if (mInjector == null) {
return;
}
mInjector.unregisterDisplayListener(mListener);
unscheduleUpdate();
}
public ResolutionPreferenceFragment() {}
@VisibleForTesting
ResolutionPreferenceFragment(@NonNull Injector injector) {
mInjector = injector;
}
@VisibleForTesting
protected int getDisplayIdArg() {
var args = getArguments();
return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY;
}
@VisibleForTesting
@NonNull
protected Resources getResources(@NonNull Context context) {
return context.getResources();
}
private void update() {
final PreferenceScreen screen = getPreferenceScreen();
if (screen == null || mInjector == null) {
return;
}
var context = mInjector.getContext();
if (context == null) {
return;
}
var display = mInjector.getDisplay(getDisplayIdArg());
if (display == null) {
screen.removeAll();
mTopOptionsPreference = null;
mMoreOptionsPreference = null;
return;
}
mResolutionPreferences.clear();
var remainingModes = addModePreferences(context,
getTopPreference(context, screen),
display.getSupportedModes(), this::isTopMode, display);
addRemainingPreferences(context,
getMorePreference(context, screen),
display, remainingModes.first, remainingModes.second);
}
private PreferenceCategory getTopPreference(@NonNull Context context,
@NonNull PreferenceScreen screen) {
if (mTopOptionsPreference == null) {
mTopOptionsPreference = new PreferenceCategory(context);
mTopOptionsPreference.setPersistent(false);
mTopOptionsPreference.setKey(TOP_OPTIONS_KEY);
screen.addPreference(mTopOptionsPreference);
} else {
mTopOptionsPreference.removeAll();
}
return mTopOptionsPreference;
}
private PreferenceCategory getMorePreference(@NonNull Context context,
@NonNull PreferenceScreen screen) {
if (mMoreOptionsPreference == null) {
mMoreOptionsPreference = new PreferenceCategory(context);
mMoreOptionsPreference.setPersistent(false);
mMoreOptionsPreference.setTitle(MORE_OPTIONS_TITLE_RESOURCE);
mMoreOptionsPreference.setOnExpandButtonClickListener(() -> {
mMoreOptionsExpanded = true;
});
mMoreOptionsPreference.setKey(MORE_OPTIONS_KEY);
screen.addPreference(mMoreOptionsPreference);
} else {
mMoreOptionsPreference.removeAll();
}
return mMoreOptionsPreference;
}
private void addRemainingPreferences(@NonNull Context context,
@NonNull PreferenceCategory group, @NonNull DisplayDevice display,
boolean isSelectedModeFound, @NonNull List<Mode> moreModes) {
if (moreModes.isEmpty()) {
return;
}
mMoreOptionsExpanded |= !isSelectedModeFound;
group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0);
addModePreferences(context, group, moreModes, /*checkMode=*/ null, display);
}
private Pair<Boolean, List<Mode>> addModePreferences(@NonNull Context context,
@NonNull PreferenceGroup group,
@NonNull List<Mode> modes,
@Nullable ToBooleanFunction<Mode> checkMode,
@NonNull DisplayDevice display) {
Mode curMode = display.getMode();
var currentResolution = curMode.getPhysicalWidth() + "x" + curMode.getPhysicalHeight();
var rotatedResolution = curMode.getPhysicalHeight() + "x" + curMode.getPhysicalWidth();
var skippedModes = new ArrayList<Mode>();
var isAnyOfModesSelected = false;
for (var mode : modes) {
var modeStr = mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight();
SelectorWithWidgetPreference pref = group.findPreference(modeStr);
if (pref != null) {
continue;
}
if (checkMode != null && !checkMode.apply(mode)) {
skippedModes.add(mode);
continue;
}
var isCurrentMode =
currentResolution.equals(modeStr) || rotatedResolution.equals(modeStr);
if (!isCurrentMode && !isAllowedMode(mode)) {
continue;
}
if (mResolutionPreferences.contains(modeStr)) {
// Added to "Top modes" already.
continue;
}
mResolutionPreferences.add(modeStr);
pref = new SelectorWithWidgetPreference(context);
pref.setPersistent(false);
pref.setKey(modeStr);
pref.setTitle(mode.getPhysicalWidth() + " x " + mode.getPhysicalHeight());
pref.setSingleLineTitle(true);
pref.setOnClickListener(preference -> onDisplayModeClicked(preference, display));
pref.setChecked(isCurrentMode);
isAnyOfModesSelected |= isCurrentMode;
group.addPreference(pref);
}
return new Pair<>(isAnyOfModesSelected, skippedModes);
}
private boolean isTopMode(@NonNull Mode mode) {
return mTopOptionsPreference != null
&& mTopOptionsPreference.getPreferenceCount() < 3;
}
private boolean isAllowedMode(@NonNull Mode mode) {
if (mRefreshRateSynchronizationEnabled
&& (mode.getRefreshRate() < DEFAULT_LOW_REFRESH_RATE - 1
|| mode.getRefreshRate() > DEFAULT_LOW_REFRESH_RATE + 1)) {
Log.d(TAG, mode + " refresh rate is out of synchronization range");
return false;
}
if (mExternalDisplayPeakHeight > 0
&& mode.getPhysicalHeight() > mExternalDisplayPeakHeight) {
Log.d(TAG, mode + " height is above the allowed limit");
return false;
}
if (mExternalDisplayPeakWidth > 0
&& mode.getPhysicalWidth() > mExternalDisplayPeakWidth) {
Log.d(TAG, mode + " width is above the allowed limit");
return false;
}
if (mExternalDisplayPeakRefreshRate > 0
&& mode.getRefreshRate() > mExternalDisplayPeakRefreshRate) {
Log.d(TAG, mode + " refresh rate is above the allowed limit");
return false;
}
return true;
}
private void scheduleUpdate() {
if (mInjector == null || !mStarted) {
return;
}
unscheduleUpdate();
mInjector.getHandler().post(mUpdateRunnable);
}
private void unscheduleUpdate() {
if (mInjector == null || !mStarted) {
return;
}
mInjector.getHandler().removeCallbacks(mUpdateRunnable);
}
private void onDisplayModeClicked(@NonNull SelectorWithWidgetPreference preference,
@NonNull DisplayDevice display) {
if (mInjector == null) {
return;
}
String[] modeResolution = preference.getKey().split("x");
int width = Integer.parseInt(modeResolution[0]);
int height = Integer.parseInt(modeResolution[1]);
for (var mode : display.getSupportedModes()) {
if (mode.getPhysicalWidth() == width && mode.getPhysicalHeight() == height
&& isAllowedMode(mode)) {
mInjector.setUserPreferredDisplayMode(display.getId(), mode);
return;
}
}
}
private boolean isDisplayResolutionLimitEnabled() {
if (mInjector == null) {
return false;
}
var flagOverride = mInjector.getSystemProperty(DISPLAY_MODE_LIMIT_OVERRIDE_PROP);
var isOverrideEnabled = "true".equals(flagOverride);
var isOverrideEnabledOrNotSet = !"false".equals(flagOverride);
return (mInjector.isModeLimitForExternalDisplayEnabled() && isOverrideEnabledOrNotSet)
|| isOverrideEnabled;
}
private void updateDisplayModeLimits(@Nullable Context context) {
if (context == null) {
return;
}
mExternalDisplayPeakRefreshRate = getResources(context).getInteger(
com.android.internal.R.integer.config_externalDisplayPeakRefreshRate);
if (isDisplayResolutionLimitEnabled()) {
mExternalDisplayPeakWidth = getResources(context).getInteger(
com.android.internal.R.integer.config_externalDisplayPeakWidth);
mExternalDisplayPeakHeight = getResources(context).getInteger(
com.android.internal.R.integer.config_externalDisplayPeakHeight);
}
mRefreshRateSynchronizationEnabled = getResources(context).getBoolean(
com.android.internal.R.bool.config_refreshRateSynchronizationEnabled);
Log.d(TAG, "mExternalDisplayPeakRefreshRate=" + mExternalDisplayPeakRefreshRate);
Log.d(TAG, "mExternalDisplayPeakWidth=" + mExternalDisplayPeakWidth);
Log.d(TAG, "mExternalDisplayPeakHeight=" + mExternalDisplayPeakHeight);
Log.d(TAG, "mRefreshRateSynchronizationEnabled=" + mRefreshRateSynchronizationEnabled);
}
}