Merge "Added External Display settings page" into main

This commit is contained in:
Oleg Blinnikov
2024-06-25 15:01:01 +00:00
committed by Android (Google) Code Review
20 changed files with 2708 additions and 16 deletions

View File

@@ -0,0 +1,94 @@
/*
* 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;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.search.Indexable;
/**
* Base class for fragment suitable for unit testing.
*/
public abstract class SettingsPreferenceFragmentBase extends SettingsPreferenceFragment
implements Indexable {
@Override
@SuppressWarnings({"RequiresNullabilityAnnotation"})
public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
onCreateCallback(icicle);
}
@Override
@SuppressWarnings({"RequiresNullabilityAnnotation"})
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
onActivityCreatedCallback(savedInstanceState);
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
onSaveInstanceStateCallback(outState);
}
@Override
public void onStart() {
super.onStart();
onStartCallback();
}
@Override
public void onStop() {
super.onStop();
onStopCallback();
}
protected Activity getCurrentActivity() {
return getActivity();
}
/**
* Callback called from {@link #onCreate}
*/
public abstract void onCreateCallback(@Nullable Bundle icicle);
/**
* Callback called from {@link #onActivityCreated}
*/
public abstract void onActivityCreatedCallback(@Nullable Bundle savedInstanceState);
/**
* Callback called from {@link #onStart}
*/
public abstract void onStartCallback();
/**
* Callback called from {@link #onStop}
*/
public abstract void onStopCallback();
/**
* Callback called from {@link #onSaveInstanceState}
*/
public void onSaveInstanceStateCallback(@NonNull final Bundle outState) {
// Do nothing.
}
}

View File

@@ -15,6 +15,8 @@
*/
package com.android.settings.connecteddevice;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isExternalDisplaySettingsPageEnabled;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.input.InputManager;
@@ -22,6 +24,8 @@ import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.InputDevice;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
@@ -31,12 +35,15 @@ import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.display.ExternalDisplayUpdater;
import com.android.settings.connecteddevice.dock.DockUpdater;
import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.FeatureFlags;
import com.android.settings.flags.FeatureFlagsImpl;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.DockUpdaterFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
@@ -64,6 +71,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
@VisibleForTesting
PreferenceGroup mPreferenceGroup;
@Nullable
private ExternalDisplayUpdater mExternalDisplayUpdater;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater;
private DockUpdater mConnectedDockUpdater;
@@ -71,6 +80,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
private final PackageManager mPackageManager;
private final InputManager mInputManager;
private final LocalBluetoothManager mLocalBluetoothManager;
@NonNull
private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl();
public ConnectedDeviceGroupController(Context context) {
super(context, KEY);
@@ -81,6 +92,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
@Override
public void onStart() {
if (mExternalDisplayUpdater != null) {
mExternalDisplayUpdater.registerCallback();
}
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.registerCallback();
mBluetoothDeviceUpdater.refreshPreference();
@@ -101,6 +116,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
@Override
public void onStop() {
if (mExternalDisplayUpdater != null) {
mExternalDisplayUpdater.unregisterCallback();
}
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.unregisterCallback();
}
@@ -127,6 +146,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
if (isAvailable()) {
final Context context = screen.getContext();
if (mExternalDisplayUpdater != null) {
mExternalDisplayUpdater.initPreference(context);
}
if (mBluetoothDeviceUpdater != null) {
mBluetoothDeviceUpdater.setPrefContext(context);
mBluetoothDeviceUpdater.forceUpdate();
@@ -150,7 +173,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
@Override
public int getAvailabilityStatus() {
return (hasBluetoothFeature()
return (hasExternalDisplayFeature()
|| hasBluetoothFeature()
|| hasUsbFeature()
|| hasUsiStylusFeature()
|| mConnectedDockUpdater != null)
@@ -180,11 +204,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
}
@VisibleForTesting
void init(BluetoothDeviceUpdater bluetoothDeviceUpdater,
void init(@Nullable ExternalDisplayUpdater externalDisplayUpdater,
BluetoothDeviceUpdater bluetoothDeviceUpdater,
ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater,
DockUpdater connectedDockUpdater,
StylusDeviceUpdater connectedStylusDeviceUpdater) {
mExternalDisplayUpdater = externalDisplayUpdater;
mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
mConnectedUsbDeviceUpdater = connectedUsbDeviceUpdater;
mConnectedDockUpdater = connectedDockUpdater;
@@ -197,7 +223,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
FeatureFactory.getFeatureFactory().getDockUpdaterFeatureProvider();
final DockUpdater connectedDockUpdater =
dockUpdaterFeatureProvider.getConnectedDockUpdater(context, this);
init(hasBluetoothFeature()
init(hasExternalDisplayFeature()
? new ExternalDisplayUpdater(this, fragment.getMetricsCategory())
: null,
hasBluetoothFeature()
? new ConnectedBluetoothDeviceUpdater(context, this,
fragment.getMetricsCategory())
: null,
@@ -210,6 +239,19 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
: null);
}
/**
* @return trunk stable feature flags.
*/
@VisibleForTesting
@NonNull
public FeatureFlags getFeatureFlags() {
return mFeatureFlags;
}
private boolean hasExternalDisplayFeature() {
return isExternalDisplaySettingsPageEnabled(getFeatureFlags());
}
private boolean hasBluetoothFeature() {
return mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
}

View File

@@ -0,0 +1,544 @@
/*
* 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.EXTERNAL_DISPLAY_HELP_URL;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isUseDisplaySettingEnabled;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isResolutionSettingEnabled;
import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isRotationSettingEnabled;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.view.Display;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
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.settings.core.SubSettingLauncher;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.Indexable;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.MainSwitchPreference;
import com.android.settingslib.widget.TwoTargetPreference;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* The Settings screen for External Displays configuration and connection management.
*/
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase
implements Indexable {
static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings;
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
static final String DISPLAYS_LIST_PREFERENCE_KEY = "displays_list_preference";
static final String EXTERNAL_DISPLAY_USE_PREFERENCE_KEY = "external_display_use_preference";
static final String EXTERNAL_DISPLAY_ROTATION_KEY = "external_display_rotation";
static final String EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY = "external_display_resolution";
static final int EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE =
R.string.external_display_change_resolution_footer_title;
static final int EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE =
R.drawable.external_display_mirror_landscape;
static final int EXTERANAL_DISPLAY_TITLE_RESOURCE =
R.string.external_display_settings_title;
static final int EXTERNAL_DISPLAY_USE_TITLE_RESOURCE =
R.string.external_display_use_title;
static final int EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE =
R.string.external_display_not_found_footer_title;
static final int EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE =
R.drawable.external_display_mirror_portrait;
static final int EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE =
R.string.external_display_rotation;
static final int EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE =
R.string.external_display_resolution_settings_title;
@VisibleForTesting
static final String PREVIOUSLY_SHOWN_LIST_KEY = "mPreviouslyShownListOfDisplays";
private boolean mStarted;
@Nullable
private MainSwitchPreference mUseDisplayPref;
@Nullable
private IllustrationPreference mImagePreference;
@Nullable
private Preference mResolutionPreference;
@Nullable
private ListPreference mRotationPref;
@Nullable
private FooterPreference mFooterPreference;
@Nullable
private PreferenceCategory mDisplaysPreference;
@Nullable
private Injector mInjector;
@Nullable
private String[] mRotationEntries;
@Nullable
private String[] mRotationEntriesValues;
@NonNull
private final Runnable mUpdateRunnable = this::update;
private final DisplayListener mListener = new DisplayListener() {
@Override
public void update(int displayId) {
scheduleUpdate();
}
};
private boolean mPreviouslyShownListOfDisplays;
public ExternalDisplayPreferenceFragment() {}
@VisibleForTesting
ExternalDisplayPreferenceFragment(@NonNull Injector injector) {
mInjector = injector;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
}
@Override
public int getHelpResource() {
return EXTERNAL_DISPLAY_HELP_URL;
}
@Override
public void onSaveInstanceStateCallback(@NonNull Bundle outState) {
outState.putSerializable(PREVIOUSLY_SHOWN_LIST_KEY,
(Serializable) mPreviouslyShownListOfDisplays);
}
@Override
public void onCreateCallback(@Nullable Bundle icicle) {
if (mInjector == null) {
mInjector = new Injector(getPrefContext());
}
addPreferencesFromResource(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
}
@Override
public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) {
restoreState(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();
}
/**
* @return id of the preference.
*/
@Override
protected int getPreferenceScreenResId() {
return EXTERNAL_DISPLAY_SETTINGS_RESOURCE;
}
@VisibleForTesting
protected void launchResolutionSelector(@NonNull final Context context, final int displayId) {
final Bundle args = new Bundle();
args.putInt(DISPLAY_ID_ARG, displayId);
new SubSettingLauncher(context)
.setDestination(ResolutionPreferenceFragment.class.getName())
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory()).launch();
}
@VisibleForTesting
protected void launchDisplaySettings(final int displayId) {
final Bundle args = new Bundle();
var context = getPrefContext();
args.putInt(DISPLAY_ID_ARG, displayId);
new SubSettingLauncher(context)
.setDestination(this.getClass().getName())
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory()).launch();
}
/**
* Returns the preference for the footer.
*/
@NonNull
@VisibleForTesting
FooterPreference getFooterPreference(@NonNull Context context) {
if (mFooterPreference == null) {
mFooterPreference = new FooterPreference(context);
mFooterPreference.setPersistent(false);
}
return mFooterPreference;
}
@NonNull
@VisibleForTesting
ListPreference getRotationPreference(@NonNull Context context) {
if (mRotationPref == null) {
mRotationPref = new ListPreference(context);
mRotationPref.setPersistent(false);
}
return mRotationPref;
}
@NonNull
@VisibleForTesting
Preference getResolutionPreference(@NonNull Context context) {
if (mResolutionPreference == null) {
mResolutionPreference = new Preference(context);
mResolutionPreference.setPersistent(false);
}
return mResolutionPreference;
}
@NonNull
@VisibleForTesting
MainSwitchPreference getUseDisplayPreference(@NonNull Context context) {
if (mUseDisplayPref == null) {
mUseDisplayPref = new MainSwitchPreference(context);
mUseDisplayPref.setPersistent(false);
}
return mUseDisplayPref;
}
@NonNull
@VisibleForTesting
IllustrationPreference getIllustrationPreference(@NonNull Context context) {
if (mImagePreference == null) {
mImagePreference = new IllustrationPreference(context);
mImagePreference.setPersistent(false);
}
return mImagePreference;
}
/**
* @return return display id argument of this settings page.
*/
@VisibleForTesting
protected int getDisplayIdArg() {
var args = getArguments();
return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY;
}
@NonNull
private PreferenceCategory getDisplaysListPreference(@NonNull Context context) {
if (mDisplaysPreference == null) {
mDisplaysPreference = new PreferenceCategory(context);
mDisplaysPreference.setPersistent(false);
}
return mDisplaysPreference;
}
private void restoreState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
mPreviouslyShownListOfDisplays = Boolean.TRUE.equals(savedInstanceState.getSerializable(
PREVIOUSLY_SHOWN_LIST_KEY, Boolean.class));
}
private void update() {
final var screen = getPreferenceScreen();
if (screen == null || mInjector == null || mInjector.getContext() == null) {
return;
}
screen.removeAll();
updateScreenForDisplayId(getDisplayIdArg(), screen, mInjector.getContext());
}
private void updateScreenForDisplayId(final int displayId,
@NonNull final PreferenceScreen screen, @NonNull Context context) {
final var displaysToShow = getDisplaysToShow(displayId);
if (displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) {
showTextWhenNoDisplaysToShow(screen, context);
} else if (displaysToShow.size() == 1
&& ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays)
|| displaysToShow.get(0).getDisplayId() == displayId)) {
showDisplaySettings(displaysToShow.get(0), screen, context);
} else if (displayId == INVALID_DISPLAY) {
// If ever shown a list of displays - keep showing it for consistency after
// disconnecting one of the displays, and only one display is left.
mPreviouslyShownListOfDisplays = true;
showDisplaysList(displaysToShow, screen, context);
}
updateSettingsTitle(displaysToShow, displayId);
}
private void updateSettingsTitle(@NonNull final List<Display> displaysToShow, int displayId) {
final Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
if (displaysToShow.size() == 1 && displaysToShow.get(0).getDisplayId() == displayId) {
var displayName = displaysToShow.get(0).getName();
if (!displayName.isEmpty()) {
activity.setTitle(displayName.substring(0, Math.min(displayName.length(), 40)));
return;
}
}
activity.setTitle(EXTERANAL_DISPLAY_TITLE_RESOURCE);
}
private void showTextWhenNoDisplaysToShow(@NonNull final PreferenceScreen screen,
@NonNull Context context) {
if (isUseDisplaySettingEnabled(mInjector)) {
screen.addPreference(updateUseDisplayPreferenceNoDisplaysFound(context));
}
screen.addPreference(updateFooterPreference(context,
EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE));
}
private void showDisplaySettings(@NonNull Display display, @NonNull PreferenceScreen screen,
@NonNull Context context) {
final var isEnabled = mInjector != null && mInjector.isDisplayEnabled(display);
if (isUseDisplaySettingEnabled(mInjector)) {
screen.addPreference(updateUseDisplayPreference(context, display, isEnabled));
}
if (!isEnabled) {
// Skip all other settings
return;
}
final var displayRotation = getDisplayRotation(display.getDisplayId());
screen.addPreference(updateIllustrationImage(context, displayRotation));
screen.addPreference(updateResolutionPreference(context, display));
screen.addPreference(updateRotationPreference(context, display, displayRotation));
if (isResolutionSettingEnabled(mInjector)) {
screen.addPreference(updateFooterPreference(context,
EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE));
}
}
private void showDisplaysList(@NonNull List<Display> displaysToShow,
@NonNull PreferenceScreen screen, @NonNull Context context) {
var pref = getDisplaysListPreference(context);
pref.setKey(DISPLAYS_LIST_PREFERENCE_KEY);
pref.removeAll();
if (!displaysToShow.isEmpty()) {
screen.addPreference(pref);
}
for (var display : displaysToShow) {
pref.addPreference(new DisplayPreference(context, display));
}
}
private List<Display> getDisplaysToShow(int displayIdToShow) {
if (mInjector == null) {
return List.of();
}
if (displayIdToShow != INVALID_DISPLAY) {
var display = mInjector.getDisplay(displayIdToShow);
if (display != null && isDisplayAllowed(display, mInjector)) {
return List.of(display);
}
}
var displaysToShow = new ArrayList<Display>();
for (var display : mInjector.getAllDisplays()) {
if (display != null && isDisplayAllowed(display, mInjector)) {
displaysToShow.add(display);
}
}
return displaysToShow;
}
private Preference updateUseDisplayPreferenceNoDisplaysFound(@NonNull Context context) {
final var pref = getUseDisplayPreference(context);
pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE);
pref.setChecked(false);
pref.setEnabled(false);
pref.setOnPreferenceChangeListener(null);
return pref;
}
private Preference updateUseDisplayPreference(@NonNull final Context context,
@NonNull final Display display, boolean isEnabled) {
final var pref = getUseDisplayPreference(context);
pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE);
pref.setChecked(isEnabled);
pref.setEnabled(true);
pref.setOnPreferenceChangeListener((p, newValue) -> {
writePreferenceClickMetric(p);
final boolean result;
if (mInjector == null) {
return false;
}
if ((Boolean) newValue) {
result = mInjector.enableConnectedDisplay(display.getDisplayId());
} else {
result = mInjector.disableConnectedDisplay(display.getDisplayId());
}
if (result) {
pref.setChecked((Boolean) newValue);
}
return result;
});
return pref;
}
private Preference updateIllustrationImage(@NonNull final Context context,
final int displayRotation) {
var pref = getIllustrationPreference(context);
if (displayRotation % 2 == 0) {
pref.setLottieAnimationResId(EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE);
} else {
pref.setLottieAnimationResId(EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE);
}
return pref;
}
private Preference updateFooterPreference(@NonNull final Context context, final int title) {
var pref = getFooterPreference(context);
pref.setTitle(title);
return pref;
}
private Preference updateRotationPreference(@NonNull final Context context,
@NonNull final Display display, final int displayRotation) {
var pref = getRotationPreference(context);
pref.setKey(EXTERNAL_DISPLAY_ROTATION_KEY);
pref.setTitle(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE);
if (mRotationEntries == null || mRotationEntriesValues == null) {
mRotationEntries = new String[] {
context.getString(R.string.external_display_standard_rotation),
context.getString(R.string.external_display_rotation_90),
context.getString(R.string.external_display_rotation_180),
context.getString(R.string.external_display_rotation_270)};
mRotationEntriesValues = new String[] {"0", "1", "2", "3"};
}
pref.setEntries(mRotationEntries);
pref.setEntryValues(mRotationEntriesValues);
pref.setValueIndex(displayRotation);
pref.setSummary(mRotationEntries[displayRotation]);
pref.setOnPreferenceChangeListener((p, newValue) -> {
writePreferenceClickMetric(p);
var rotation = Integer.parseInt((String) newValue);
var displayId = display.getDisplayId();
if (mInjector == null || !mInjector.freezeDisplayRotation(displayId, rotation)) {
return false;
}
pref.setValueIndex(rotation);
return true;
});
pref.setEnabled(isRotationSettingEnabled(mInjector));
return pref;
}
private Preference updateResolutionPreference(@NonNull final Context context,
@NonNull final Display display) {
var pref = getResolutionPreference(context);
pref.setKey(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
pref.setTitle(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE);
pref.setSummary(display.getMode().getPhysicalWidth() + " x "
+ display.getMode().getPhysicalHeight());
pref.setOnPreferenceClickListener((Preference p) -> {
writePreferenceClickMetric(p);
launchResolutionSelector(context, display.getDisplayId());
return true;
});
pref.setEnabled(isResolutionSettingEnabled(mInjector));
return pref;
}
private int getDisplayRotation(int displayId) {
if (mInjector == null) {
return 0;
}
return Math.min(3, Math.max(0, mInjector.getDisplayUserRotation(displayId)));
}
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);
}
@VisibleForTesting
class DisplayPreference extends TwoTargetPreference
implements Preference.OnPreferenceClickListener {
private final int mDisplayId;
DisplayPreference(@NonNull final Context context, @NonNull final Display display) {
super(context);
mDisplayId = display.getDisplayId();
setPersistent(false);
setKey("display_id_" + mDisplayId);
setTitle(display.getName());
setSummary(display.getMode().getPhysicalWidth() + " x "
+ display.getMode().getPhysicalHeight());
setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
launchDisplaySettings(mDisplayId);
writePreferenceClickMetric(preference);
return true;
}
}
}

View File

@@ -0,0 +1,341 @@
/*
* 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.content.Context.DISPLAY_SERVICE;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
import static android.view.Display.INVALID_DISPLAY;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.view.Display;
import android.view.Display.Mode;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.flags.FeatureFlags;
import com.android.settings.flags.FeatureFlagsImpl;
public class ExternalDisplaySettingsConfiguration {
static final String VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY =
"persist.demo.userrotation.package_name";
static final String DISPLAY_ID_ARG = "display_id";
static final int EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE = R.string.external_display_not_found;
static final int EXTERNAL_DISPLAY_HELP_URL = R.string.help_url_external_display;
public static class SystemServicesProvider {
@Nullable
private IWindowManager mWindowManager;
@Nullable
private DisplayManager mDisplayManager;
@Nullable
protected Context mContext;
/**
* @param name of a system property.
* @return the value of the system property.
*/
@NonNull
public String getSystemProperty(@NonNull String name) {
return SystemProperties.get(name);
}
/**
* @return return public Display manager.
*/
@Nullable
public DisplayManager getDisplayManager() {
if (mDisplayManager == null && getContext() != null) {
mDisplayManager = (DisplayManager) getContext().getSystemService(DISPLAY_SERVICE);
}
return mDisplayManager;
}
/**
* @return internal IWindowManager
*/
@Nullable
public IWindowManager getWindowManager() {
if (mWindowManager == null) {
mWindowManager = WindowManagerGlobal.getWindowManagerService();
}
return mWindowManager;
}
/**
* @return context.
*/
@Nullable
public Context getContext() {
return mContext;
}
}
public static class Injector extends SystemServicesProvider {
@NonNull
private final FeatureFlags mFlags;
@NonNull
private final Handler mHandler;
Injector(@Nullable Context context) {
this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper()));
}
Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) {
mContext = context;
mFlags = flags;
mHandler = handler;
}
/**
* @return all displays including disabled.
*/
@NonNull
public Display[] getAllDisplays() {
var dm = getDisplayManager();
if (dm == null) {
return new Display[0];
}
return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
}
/**
* @return enabled displays only.
*/
@NonNull
public Display[] getEnabledDisplays() {
var dm = getDisplayManager();
if (dm == null) {
return new Display[0];
}
return dm.getDisplays();
}
/**
* @return true if the display is enabled
*/
public boolean isDisplayEnabled(@NonNull Display display) {
for (var enabledDisplay : getEnabledDisplays()) {
if (enabledDisplay.getDisplayId() == display.getDisplayId()) {
return true;
}
}
return false;
}
/**
* Register display listener.
*/
public void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener) {
var dm = getDisplayManager();
if (dm == null) {
return;
}
dm.registerDisplayListener(listener, mHandler, EVENT_FLAG_DISPLAY_ADDED
| EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED
| EVENT_FLAG_DISPLAY_CONNECTION_CHANGED);
}
/**
* Unregister display listener.
*/
public void unregisterDisplayListener(@NonNull DisplayManager.DisplayListener listener) {
var dm = getDisplayManager();
if (dm == null) {
return;
}
dm.unregisterDisplayListener(listener);
}
/**
* @return feature flags.
*/
@NonNull
public FeatureFlags getFlags() {
return mFlags;
}
/**
* Enable connected display.
*/
public boolean enableConnectedDisplay(int displayId) {
var dm = getDisplayManager();
if (dm == null) {
return false;
}
dm.enableConnectedDisplay(displayId);
return true;
}
/**
* Disable connected display.
*/
public boolean disableConnectedDisplay(int displayId) {
var dm = getDisplayManager();
if (dm == null) {
return false;
}
dm.disableConnectedDisplay(displayId);
return true;
}
/**
* @param displayId which must be returned
* @return display object for the displayId
*/
@Nullable
public Display getDisplay(int displayId) {
if (displayId == INVALID_DISPLAY) {
return null;
}
var dm = getDisplayManager();
if (dm == null) {
return null;
}
return dm.getDisplay(displayId);
}
/**
* @return handler
*/
@NonNull
public Handler getHandler() {
return mHandler;
}
/**
* Get display rotation
* @param displayId display identifier
* @return rotation
*/
public int getDisplayUserRotation(int displayId) {
var wm = getWindowManager();
if (wm == null) {
return 0;
}
try {
return wm.getDisplayUserRotation(displayId);
} catch (RemoteException e) {
return 0;
}
}
/**
* Freeze rotation of the display in the specified rotation.
* @param displayId display identifier
* @param rotation [0, 1, 2, 3]
* @return true if successful
*/
public boolean freezeDisplayRotation(int displayId, int rotation) {
var wm = getWindowManager();
if (wm == null) {
return false;
}
try {
wm.freezeDisplayRotation(displayId, rotation,
"ExternalDisplayPreferenceFragment");
return true;
} catch (RemoteException e) {
return false;
}
}
/**
* Enforce display mode on the given display.
*/
public void setUserPreferredDisplayMode(int displayId, @NonNull Mode mode) {
DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode);
}
}
public abstract static class DisplayListener implements DisplayManager.DisplayListener {
@Override
public void onDisplayAdded(int displayId) {
update(displayId);
}
@Override
public void onDisplayRemoved(int displayId) {
update(displayId);
}
@Override
public void onDisplayChanged(int displayId) {
update(displayId);
}
@Override
public void onDisplayConnected(int displayId) {
update(displayId);
}
@Override
public void onDisplayDisconnected(int displayId) {
update(displayId);
}
/**
* Called from other listener methods to trigger update of the settings page.
*/
public abstract void update(int displayId);
}
/**
* @return whether the settings page is enabled or not.
*/
public static boolean isExternalDisplaySettingsPageEnabled(@NonNull FeatureFlags flags) {
return flags.rotationConnectedDisplaySetting()
|| flags.resolutionAndEnableConnectedDisplaySetting();
}
static boolean isDisplayAllowed(@NonNull Display display,
@NonNull SystemServicesProvider props) {
return display.getType() == Display.TYPE_EXTERNAL
|| display.getType() == Display.TYPE_OVERLAY
|| isVirtualDisplayAllowed(display, props);
}
static boolean isVirtualDisplayAllowed(@NonNull Display display,
@NonNull SystemServicesProvider properties) {
var sysProp = properties.getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY);
return !sysProp.isEmpty() && display.getType() == Display.TYPE_VIRTUAL
&& sysProp.equals(display.getOwnerPackageName());
}
static boolean isUseDisplaySettingEnabled(@Nullable Injector injector) {
return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting();
}
static boolean isResolutionSettingEnabled(@Nullable Injector injector) {
return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting();
}
static boolean isRotationSettingEnabled(@Nullable Injector injector) {
return injector != null && injector.getFlags().rotationConnectedDisplaySetting();
}
}

View File

@@ -0,0 +1,181 @@
/*
* 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 com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class ExternalDisplayUpdater {
private static final String PREF_KEY = "external_display_settings";
private final int mMetricsCategory;
@NonNull
private final MetricsFeatureProvider mMetricsFeatureProvider;
@NonNull
private final Runnable mUpdateRunnable = this::update;
@NonNull
private final DevicePreferenceCallback mDevicePreferenceCallback;
@Nullable
private RestrictedPreference mPreference;
@Nullable
private Injector mInjector;
private final DisplayListener mListener = new DisplayListener() {
@Override
public void update(int displayId) {
scheduleUpdate();
}
};
public ExternalDisplayUpdater(@NonNull DevicePreferenceCallback callback, int metricsCategory) {
mDevicePreferenceCallback = callback;
mMetricsCategory = metricsCategory;
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
/**
* Set the context to generate the {@link Preference}, so it could get the correct theme.
*/
public void initPreference(@NonNull Context context) {
initPreference(context, new Injector(context));
}
@VisibleForTesting
void initPreference(@NonNull Context context, Injector injector) {
mInjector = injector;
mPreference = new RestrictedPreference(context, null /* AttributeSet */);
mPreference.setTitle(R.string.external_display_settings_title);
mPreference.setSummary(getSummary());
mPreference.setIcon(getDrawable(context));
mPreference.setKey(PREF_KEY);
mPreference.setDisabledByAdmin(checkIfUsbDataSignalingIsDisabled(context));
mPreference.setOnPreferenceClickListener((Preference p) -> {
mMetricsFeatureProvider.logClickedPreference(p, mMetricsCategory);
// New version - uses a separate screen.
new SubSettingLauncher(context)
.setDestination(ExternalDisplayPreferenceFragment.class.getName())
.setTitleRes(R.string.external_display_settings_title)
.setSourceMetricsCategory(mMetricsCategory)
.launch();
return true;
});
scheduleUpdate();
}
/**
* Unregister the display listener.
*/
public void unregisterCallback() {
if (mInjector != null) {
mInjector.unregisterDisplayListener(mListener);
}
}
/**
* Register the display listener.
*/
public void registerCallback() {
if (mInjector != null) {
mInjector.registerDisplayListener(mListener);
}
}
@VisibleForTesting
@Nullable
protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled(Context context) {
return RestrictedLockUtilsInternal.checkIfUsbDataSignalingIsDisabled(context,
UserHandle.myUserId());
}
@VisibleForTesting
@Nullable
protected Drawable getDrawable(Context context) {
return context.getDrawable(R.drawable.ic_external_display_32dp);
}
@Nullable
protected CharSequence getSummary() {
if (mInjector == null) {
return null;
}
var context = mInjector.getContext();
if (context == null) {
return null;
}
for (var display : mInjector.getEnabledDisplays()) {
if (display != null && isDisplayAllowed(display, mInjector)) {
return context.getString(R.string.external_display_on);
}
}
for (var display : mInjector.getAllDisplays()) {
if (display != null && isDisplayAllowed(display, mInjector)) {
return context.getString(R.string.external_display_off);
}
}
return null;
}
private void scheduleUpdate() {
if (mInjector == null) {
return;
}
unscheduleUpdate();
mInjector.getHandler().post(mUpdateRunnable);
}
private void unscheduleUpdate() {
if (mInjector == null) {
return;
}
mInjector.getHandler().removeCallbacks(mUpdateRunnable);
}
private void update() {
var summary = getSummary();
if (mPreference == null) {
return;
}
mPreference.setSummary(summary);
if (summary != null) {
mDevicePreferenceCallback.onDeviceAdded(mPreference);
} else {
mDevicePreferenceCallback.onDeviceRemoved(mPreference);
}
}
}

View File

@@ -0,0 +1,7 @@
# Default reviewers for this and subdirectories.
santoscordon@google.com
petsjonkin@google.com
flc@google.com
wilczynskip@google.com
brup@google.com
olb@google.com

View File

@@ -0,0 +1,343 @@
/*
* 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 static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
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;
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;
public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase {
private static final String TAG = "ResolutionPreferenceFragment";
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;
@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 || !isDisplayAllowed(display, mInjector)) {
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 Display display,
boolean isSelectedModeFound, @NonNull Mode[] moreModes) {
if (moreModes.length == 0) {
return;
}
mMoreOptionsExpanded |= !isSelectedModeFound;
group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0);
addModePreferences(context, group, moreModes, /*checkMode=*/ null, display);
}
private Pair<Boolean, Mode[]> addModePreferences(@NonNull Context context,
@NonNull PreferenceGroup group,
@NonNull Mode[] modes,
@Nullable ToBooleanFunction<Mode> checkMode,
@NonNull Display display) {
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.toArray(Mode.EMPTY_ARRAY));
}
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 Display 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.getDisplayId(), mode);
return;
}
}
}
private void updateDisplayModeLimits(@Nullable Context context) {
if (context == null) {
return;
}
mExternalDisplayPeakRefreshRate = getResources(context).getInteger(
com.android.internal.R.integer.config_externalDisplayPeakRefreshRate);
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);
}
}