• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.connecteddevice.display;
18 
19 import static android.view.Display.INVALID_DISPLAY;
20 
21 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
22 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL;
23 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
24 
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.os.Bundle;
29 import android.util.Log;
30 import android.util.Pair;
31 import android.view.Display.Mode;
32 import android.view.View;
33 import android.widget.TextView;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.preference.PreferenceCategory;
39 import androidx.preference.PreferenceGroup;
40 import androidx.preference.PreferenceScreen;
41 
42 import com.android.internal.util.ToBooleanFunction;
43 import com.android.settings.R;
44 import com.android.settings.SettingsPreferenceFragmentBase;
45 import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
46 import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
47 import com.android.settingslib.widget.SelectorWithWidgetPreference;
48 
49 import java.util.ArrayList;
50 import java.util.HashSet;
51 import java.util.List;
52 
53 public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase {
54     private static final String TAG = "ResolutionPreference";
55     static final int DEFAULT_LOW_REFRESH_RATE = 60;
56     static final String MORE_OPTIONS_KEY = "more_options";
57     static final String TOP_OPTIONS_KEY = "top_options";
58     static final int MORE_OPTIONS_TITLE_RESOURCE =
59             R.string.external_display_more_options_title;
60     static final int EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE =
61             R.xml.external_display_resolution_settings;
62     static final String DISPLAY_MODE_LIMIT_OVERRIDE_PROP = "persist.sys.com.android.server.display"
63             + ".feature.flags.enable_mode_limit_for_external_display-override";
64     @Nullable
65     private Injector mInjector;
66     @Nullable
67     private PreferenceCategory mTopOptionsPreference;
68     @Nullable
69     private PreferenceCategory mMoreOptionsPreference;
70     private boolean mStarted;
71     private final HashSet<String> mResolutionPreferences = new HashSet<>();
72     private int mExternalDisplayPeakWidth;
73     private int mExternalDisplayPeakHeight;
74     private int mExternalDisplayPeakRefreshRate;
75     private boolean mRefreshRateSynchronizationEnabled;
76     private boolean mMoreOptionsExpanded;
77     private final Runnable mUpdateRunnable = this::update;
78     private final DisplayListener mListener = new DisplayListener() {
79         @Override
80         public void update(int displayId) {
81             scheduleUpdate();
82         }
83     };
84 
85     @Override
getMetricsCategory()86     public int getMetricsCategory() {
87         return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
88     }
89 
90     @Override
getHelpResource()91     public int getHelpResource() {
92         return EXTERNAL_DISPLAY_HELP_URL;
93     }
94 
95     @Override
onCreateCallback(@ullable Bundle icicle)96     public void onCreateCallback(@Nullable Bundle icicle) {
97         if (mInjector == null) {
98             mInjector = new Injector(getPrefContext());
99         }
100         addPreferencesFromResource(EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE);
101         updateDisplayModeLimits(mInjector.getContext());
102     }
103 
104     @Override
onActivityCreatedCallback(@ullable Bundle savedInstanceState)105     public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) {
106         View view = getView();
107         TextView emptyView = null;
108         if (view != null) {
109             emptyView = (TextView) view.findViewById(android.R.id.empty);
110         }
111         if (emptyView != null) {
112             emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE);
113             setEmptyView(emptyView);
114         }
115     }
116 
117     @Override
onStartCallback()118     public void onStartCallback() {
119         mStarted = true;
120         if (mInjector == null) {
121             return;
122         }
123         mInjector.registerDisplayListener(mListener);
124         scheduleUpdate();
125     }
126 
127     @Override
onStopCallback()128     public void onStopCallback() {
129         mStarted = false;
130         if (mInjector == null) {
131             return;
132         }
133         mInjector.unregisterDisplayListener(mListener);
134         unscheduleUpdate();
135     }
136 
ResolutionPreferenceFragment()137     public ResolutionPreferenceFragment() {}
138 
139     @VisibleForTesting
ResolutionPreferenceFragment(@onNull Injector injector)140     ResolutionPreferenceFragment(@NonNull Injector injector) {
141         mInjector = injector;
142     }
143 
144     @VisibleForTesting
getDisplayIdArg()145     protected int getDisplayIdArg() {
146         var args = getArguments();
147         return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY;
148     }
149 
150     @VisibleForTesting
151     @NonNull
getResources(@onNull Context context)152     protected Resources getResources(@NonNull Context context) {
153         return context.getResources();
154     }
155 
update()156     private void update() {
157         final PreferenceScreen screen = getPreferenceScreen();
158         if (screen == null || mInjector == null) {
159             return;
160         }
161         var context = mInjector.getContext();
162         if (context == null) {
163             return;
164         }
165         var display = mInjector.getDisplay(getDisplayIdArg());
166         if (display == null) {
167             screen.removeAll();
168             mTopOptionsPreference = null;
169             mMoreOptionsPreference = null;
170             return;
171         }
172         mResolutionPreferences.clear();
173         var remainingModes = addModePreferences(context,
174                 getTopPreference(context, screen),
175                 display.getSupportedModes(), this::isTopMode, display);
176         addRemainingPreferences(context,
177                 getMorePreference(context, screen),
178                 display, remainingModes.first, remainingModes.second);
179     }
180 
getTopPreference(@onNull Context context, @NonNull PreferenceScreen screen)181     private PreferenceCategory getTopPreference(@NonNull Context context,
182             @NonNull PreferenceScreen screen) {
183         if (mTopOptionsPreference == null) {
184             mTopOptionsPreference = new PreferenceCategory(context);
185             mTopOptionsPreference.setPersistent(false);
186             mTopOptionsPreference.setKey(TOP_OPTIONS_KEY);
187             screen.addPreference(mTopOptionsPreference);
188         } else {
189             mTopOptionsPreference.removeAll();
190         }
191         return mTopOptionsPreference;
192     }
193 
getMorePreference(@onNull Context context, @NonNull PreferenceScreen screen)194     private PreferenceCategory getMorePreference(@NonNull Context context,
195             @NonNull PreferenceScreen screen) {
196         if (mMoreOptionsPreference == null) {
197             mMoreOptionsPreference = new PreferenceCategory(context);
198             mMoreOptionsPreference.setPersistent(false);
199             mMoreOptionsPreference.setTitle(MORE_OPTIONS_TITLE_RESOURCE);
200             mMoreOptionsPreference.setOnExpandButtonClickListener(() -> {
201                 mMoreOptionsExpanded = true;
202             });
203             mMoreOptionsPreference.setKey(MORE_OPTIONS_KEY);
204             screen.addPreference(mMoreOptionsPreference);
205         } else {
206             mMoreOptionsPreference.removeAll();
207         }
208         return mMoreOptionsPreference;
209     }
210 
addRemainingPreferences(@onNull Context context, @NonNull PreferenceCategory group, @NonNull DisplayDevice display, boolean isSelectedModeFound, @NonNull List<Mode> moreModes)211     private void addRemainingPreferences(@NonNull Context context,
212             @NonNull PreferenceCategory group, @NonNull DisplayDevice display,
213             boolean isSelectedModeFound, @NonNull List<Mode> moreModes) {
214         if (moreModes.isEmpty()) {
215             return;
216         }
217         mMoreOptionsExpanded |= !isSelectedModeFound;
218         group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0);
219         addModePreferences(context, group, moreModes, /*checkMode=*/ null, display);
220     }
221 
addModePreferences(@onNull Context context, @NonNull PreferenceGroup group, @NonNull List<Mode> modes, @Nullable ToBooleanFunction<Mode> checkMode, @NonNull DisplayDevice display)222     private Pair<Boolean, List<Mode>> addModePreferences(@NonNull Context context,
223             @NonNull PreferenceGroup group,
224             @NonNull List<Mode> modes,
225             @Nullable ToBooleanFunction<Mode> checkMode,
226             @NonNull DisplayDevice display) {
227         Mode curMode = display.getMode();
228         var currentResolution = curMode.getPhysicalWidth() + "x" + curMode.getPhysicalHeight();
229         var rotatedResolution = curMode.getPhysicalHeight() + "x" + curMode.getPhysicalWidth();
230         var skippedModes = new ArrayList<Mode>();
231         var isAnyOfModesSelected = false;
232         for (var mode : modes) {
233             var modeStr = mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight();
234             SelectorWithWidgetPreference pref = group.findPreference(modeStr);
235             if (pref != null) {
236                 continue;
237             }
238             if (checkMode != null && !checkMode.apply(mode)) {
239                 skippedModes.add(mode);
240                 continue;
241             }
242             var isCurrentMode =
243                     currentResolution.equals(modeStr) || rotatedResolution.equals(modeStr);
244             if (!isCurrentMode && !isAllowedMode(mode)) {
245                 continue;
246             }
247             if (mResolutionPreferences.contains(modeStr)) {
248                 // Added to "Top modes" already.
249                 continue;
250             }
251             mResolutionPreferences.add(modeStr);
252             pref = new SelectorWithWidgetPreference(context);
253             pref.setPersistent(false);
254             pref.setKey(modeStr);
255             pref.setTitle(mode.getPhysicalWidth() + " x " + mode.getPhysicalHeight());
256             pref.setSingleLineTitle(true);
257             pref.setOnClickListener(preference -> onDisplayModeClicked(preference, display));
258             pref.setChecked(isCurrentMode);
259             isAnyOfModesSelected |= isCurrentMode;
260             group.addPreference(pref);
261         }
262         return new Pair<>(isAnyOfModesSelected, skippedModes);
263     }
264 
isTopMode(@onNull Mode mode)265     private boolean isTopMode(@NonNull Mode mode) {
266         return mTopOptionsPreference != null
267                 && mTopOptionsPreference.getPreferenceCount() < 3;
268     }
269 
isAllowedMode(@onNull Mode mode)270     private boolean isAllowedMode(@NonNull Mode mode) {
271         if (mRefreshRateSynchronizationEnabled
272                 && (mode.getRefreshRate() < DEFAULT_LOW_REFRESH_RATE - 1
273                         || mode.getRefreshRate() > DEFAULT_LOW_REFRESH_RATE + 1)) {
274             Log.d(TAG, mode + " refresh rate is out of synchronization range");
275             return false;
276         }
277         if (mExternalDisplayPeakHeight > 0
278                 && mode.getPhysicalHeight() > mExternalDisplayPeakHeight) {
279             Log.d(TAG, mode + " height is above the allowed limit");
280             return false;
281         }
282         if (mExternalDisplayPeakWidth > 0
283                 && mode.getPhysicalWidth() > mExternalDisplayPeakWidth) {
284             Log.d(TAG, mode + " width is above the allowed limit");
285             return false;
286         }
287         if (mExternalDisplayPeakRefreshRate > 0
288                 && mode.getRefreshRate() > mExternalDisplayPeakRefreshRate) {
289             Log.d(TAG, mode + " refresh rate is above the allowed limit");
290             return false;
291         }
292         return true;
293     }
294 
scheduleUpdate()295     private void scheduleUpdate() {
296         if (mInjector == null || !mStarted) {
297             return;
298         }
299         unscheduleUpdate();
300         mInjector.getHandler().post(mUpdateRunnable);
301     }
302 
unscheduleUpdate()303     private void unscheduleUpdate() {
304         if (mInjector == null || !mStarted) {
305             return;
306         }
307         mInjector.getHandler().removeCallbacks(mUpdateRunnable);
308     }
309 
onDisplayModeClicked(@onNull SelectorWithWidgetPreference preference, @NonNull DisplayDevice display)310     private void onDisplayModeClicked(@NonNull SelectorWithWidgetPreference preference,
311             @NonNull DisplayDevice display) {
312         if (mInjector == null) {
313             return;
314         }
315         String[] modeResolution = preference.getKey().split("x");
316         int width = Integer.parseInt(modeResolution[0]);
317         int height = Integer.parseInt(modeResolution[1]);
318         for (var mode : display.getSupportedModes()) {
319             if (mode.getPhysicalWidth() == width && mode.getPhysicalHeight() == height
320                         && isAllowedMode(mode)) {
321                 mInjector.setUserPreferredDisplayMode(display.getId(), mode);
322                 return;
323             }
324         }
325     }
326 
isDisplayResolutionLimitEnabled()327     private boolean isDisplayResolutionLimitEnabled() {
328         if (mInjector == null) {
329             return false;
330         }
331         var flagOverride = mInjector.getSystemProperty(DISPLAY_MODE_LIMIT_OVERRIDE_PROP);
332         var isOverrideEnabled = "true".equals(flagOverride);
333         var isOverrideEnabledOrNotSet = !"false".equals(flagOverride);
334         return (mInjector.isModeLimitForExternalDisplayEnabled() && isOverrideEnabledOrNotSet)
335                 || isOverrideEnabled;
336     }
337 
updateDisplayModeLimits(@ullable Context context)338     private void updateDisplayModeLimits(@Nullable Context context) {
339         if (context == null) {
340             return;
341         }
342         mExternalDisplayPeakRefreshRate = getResources(context).getInteger(
343                     com.android.internal.R.integer.config_externalDisplayPeakRefreshRate);
344         if (isDisplayResolutionLimitEnabled()) {
345             mExternalDisplayPeakWidth = getResources(context).getInteger(
346                     com.android.internal.R.integer.config_externalDisplayPeakWidth);
347             mExternalDisplayPeakHeight = getResources(context).getInteger(
348                     com.android.internal.R.integer.config_externalDisplayPeakHeight);
349         }
350         mRefreshRateSynchronizationEnabled = getResources(context).getBoolean(
351                     com.android.internal.R.bool.config_refreshRateSynchronizationEnabled);
352         Log.d(TAG, "mExternalDisplayPeakRefreshRate=" + mExternalDisplayPeakRefreshRate);
353         Log.d(TAG, "mExternalDisplayPeakWidth=" + mExternalDisplayPeakWidth);
354         Log.d(TAG, "mExternalDisplayPeakHeight=" + mExternalDisplayPeakHeight);
355         Log.d(TAG, "mRefreshRateSynchronizationEnabled=" + mRefreshRateSynchronizationEnabled);
356     }
357 }
358