1 /* 2 * Copyright (C) 2022 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.display; 18 19 import static com.android.settings.display.ScreenResolutionController.FHD_WIDTH; 20 import static com.android.settings.display.ScreenResolutionController.QHD_WIDTH; 21 22 import android.annotation.Nullable; 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.graphics.Point; 27 import android.graphics.drawable.Drawable; 28 import android.hardware.display.DisplayManager; 29 import android.provider.Settings; 30 import android.text.TextUtils; 31 import android.view.Display; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.view.accessibility.AccessibilityManager; 34 35 import androidx.annotation.VisibleForTesting; 36 import androidx.preference.PreferenceScreen; 37 38 import com.android.settings.R; 39 import com.android.settings.search.BaseSearchIndexProvider; 40 import com.android.settings.widget.RadioButtonPickerFragment; 41 import com.android.settingslib.display.DisplayDensityUtils; 42 import com.android.settingslib.search.SearchIndexable; 43 import com.android.settingslib.widget.CandidateInfo; 44 import com.android.settingslib.widget.FooterPreference; 45 import com.android.settingslib.widget.IllustrationPreference; 46 import com.android.settingslib.widget.SelectorWithWidgetPreference; 47 48 import java.util.ArrayList; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Set; 52 import java.util.concurrent.atomic.AtomicInteger; 53 54 /** Preference fragment used for switch screen resolution */ 55 @SearchIndexable 56 public class ScreenResolutionFragment extends RadioButtonPickerFragment { 57 private static final String TAG = "ScreenResolution"; 58 59 private Resources mResources; 60 private static final int FHD_INDEX = 0; 61 private static final int QHD_INDEX = 1; 62 private static final String RESOLUTION_METRIC_SETTING_KEY = "user_selected_resolution"; 63 private Display mDefaultDisplay; 64 private String[] mScreenResolutionOptions; 65 private Set<Point> mResolutions; 66 private String[] mScreenResolutionSummaries; 67 68 private IllustrationPreference mImagePreference; 69 private DisplayObserver mDisplayObserver; 70 private AccessibilityManager mAccessibilityManager; 71 72 @Override onAttach(Context context)73 public void onAttach(Context context) { 74 super.onAttach(context); 75 76 mDefaultDisplay = 77 context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY); 78 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 79 mResources = context.getResources(); 80 mScreenResolutionOptions = 81 mResources.getStringArray(R.array.config_screen_resolution_options_strings); 82 mScreenResolutionSummaries = 83 mResources.getStringArray(R.array.config_screen_resolution_summaries_strings); 84 mResolutions = getAllSupportedResolution(); 85 mImagePreference = new IllustrationPreference(context); 86 mDisplayObserver = new DisplayObserver(context); 87 } 88 89 @Override getPreferenceScreenResId()90 protected int getPreferenceScreenResId() { 91 return R.xml.screen_resolution_settings; 92 } 93 94 @Override addStaticPreferences(PreferenceScreen screen)95 protected void addStaticPreferences(PreferenceScreen screen) { 96 updateIllustrationImage(mImagePreference); 97 screen.addPreference(mImagePreference); 98 99 final FooterPreference footerPreference = new FooterPreference(screen.getContext()); 100 footerPreference.setTitle(R.string.screen_resolution_footer); 101 footerPreference.setSelectable(false); 102 footerPreference.setLayoutResource(R.layout.preference_footer); 103 screen.addPreference(footerPreference); 104 } 105 106 @Override bindPreferenceExtra( SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)107 public void bindPreferenceExtra( 108 SelectorWithWidgetPreference pref, 109 String key, 110 CandidateInfo info, 111 String defaultKey, 112 String systemDefaultKey) { 113 final ScreenResolutionCandidateInfo candidateInfo = (ScreenResolutionCandidateInfo) info; 114 final CharSequence summary = candidateInfo.loadSummary(); 115 if (summary != null) pref.setSummary(summary); 116 } 117 118 @Override getCandidates()119 protected List<? extends CandidateInfo> getCandidates() { 120 final List<ScreenResolutionCandidateInfo> candidates = new ArrayList<>(); 121 122 for (int i = 0; i < mScreenResolutionOptions.length; i++) { 123 candidates.add( 124 new ScreenResolutionCandidateInfo( 125 mScreenResolutionOptions[i], 126 mScreenResolutionSummaries[i], 127 mScreenResolutionOptions[i], 128 true /* enabled */)); 129 } 130 131 return candidates; 132 } 133 134 /** Get all supported resolutions on the device. */ getAllSupportedResolution()135 private Set<Point> getAllSupportedResolution() { 136 Set<Point> resolutions = new HashSet<>(); 137 for (Display.Mode mode : mDefaultDisplay.getSupportedModes()) { 138 resolutions.add(new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight())); 139 } 140 141 return resolutions; 142 } 143 144 /** Get prefer display mode. */ getPreferMode(int width)145 private Display.Mode getPreferMode(int width) { 146 for (Point resolution : mResolutions) { 147 if (resolution.x == width) { 148 return new Display.Mode( 149 resolution.x, resolution.y, getDisplayMode().getRefreshRate()); 150 } 151 } 152 153 return getDisplayMode(); 154 } 155 156 /** Get current display mode. */ 157 @VisibleForTesting getDisplayMode()158 public Display.Mode getDisplayMode() { 159 return mDefaultDisplay.getMode(); 160 } 161 162 /** Using display manager to set the display mode. */ 163 @VisibleForTesting setDisplayMode(final int width)164 public void setDisplayMode(final int width) { 165 mDisplayObserver.startObserve(); 166 167 /** For store settings globally. */ 168 /** TODO(b/238061217): Moving to an atom with the same string */ 169 Settings.System.putString( 170 getContext().getContentResolver(), 171 RESOLUTION_METRIC_SETTING_KEY, 172 getPreferMode(width).getPhysicalWidth() 173 + "x" 174 + getPreferMode(width).getPhysicalHeight()); 175 176 /** Apply the resolution change. */ 177 mDefaultDisplay.setUserPreferredDisplayMode(getPreferMode(width)); 178 } 179 180 /** Get the key corresponding to the resolution. */ 181 @VisibleForTesting getKeyForResolution(int width)182 String getKeyForResolution(int width) { 183 return width == FHD_WIDTH 184 ? mScreenResolutionOptions[FHD_INDEX] 185 : width == QHD_WIDTH ? mScreenResolutionOptions[QHD_INDEX] : null; 186 } 187 188 /** Get the width corresponding to the resolution key. */ getWidthForResoluitonKey(String key)189 int getWidthForResoluitonKey(String key) { 190 return mScreenResolutionOptions[FHD_INDEX].equals(key) 191 ? FHD_WIDTH 192 : mScreenResolutionOptions[QHD_INDEX].equals(key) ? QHD_WIDTH : -1; 193 } 194 195 @Override getDefaultKey()196 protected String getDefaultKey() { 197 int physicalWidth = getDisplayMode().getPhysicalWidth(); 198 199 return getKeyForResolution(physicalWidth); 200 } 201 202 @Override setDefaultKey(final String key)203 protected boolean setDefaultKey(final String key) { 204 int width = getWidthForResoluitonKey(key); 205 if (width < 0) { 206 return false; 207 } 208 209 setDisplayMode(width); 210 updateIllustrationImage(mImagePreference); 211 212 return true; 213 } 214 215 @Override onRadioButtonClicked(SelectorWithWidgetPreference selected)216 public void onRadioButtonClicked(SelectorWithWidgetPreference selected) { 217 String selectedKey = selected.getKey(); 218 int selectedWidth = getWidthForResoluitonKey(selectedKey); 219 if (!mDisplayObserver.setPendingResolutionChange(selectedWidth)) { 220 return; 221 } 222 223 if (mAccessibilityManager.isEnabled()) { 224 AccessibilityEvent event = AccessibilityEvent.obtain(); 225 event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); 226 event.getText().add(mResources.getString(R.string.screen_resolution_selected_a11y)); 227 mAccessibilityManager.sendAccessibilityEvent(event); 228 } 229 230 super.onRadioButtonClicked(selected); 231 } 232 233 /** Update the resolution image according display mode. */ updateIllustrationImage(IllustrationPreference preference)234 private void updateIllustrationImage(IllustrationPreference preference) { 235 String key = getDefaultKey(); 236 237 if (TextUtils.equals(mScreenResolutionOptions[FHD_INDEX], key)) { 238 preference.setLottieAnimationResId(R.drawable.screen_resolution_1080p); 239 } else if (TextUtils.equals(mScreenResolutionOptions[QHD_INDEX], key)) { 240 preference.setLottieAnimationResId(R.drawable.screen_resolution_1440p); 241 } 242 } 243 244 @Override getMetricsCategory()245 public int getMetricsCategory() { 246 return SettingsEnums.SCREEN_RESOLUTION; 247 } 248 249 /** This is an extension of the CandidateInfo class, which adds summary information. */ 250 public static class ScreenResolutionCandidateInfo extends CandidateInfo { 251 private final CharSequence mLabel; 252 private final CharSequence mSummary; 253 private final String mKey; 254 ScreenResolutionCandidateInfo( CharSequence label, CharSequence summary, String key, boolean enabled)255 ScreenResolutionCandidateInfo( 256 CharSequence label, CharSequence summary, String key, boolean enabled) { 257 super(enabled); 258 mLabel = label; 259 mSummary = summary; 260 mKey = key; 261 } 262 263 @Override loadLabel()264 public CharSequence loadLabel() { 265 return mLabel; 266 } 267 268 /** It is the summary for radio options. */ loadSummary()269 public CharSequence loadSummary() { 270 return mSummary; 271 } 272 273 @Override loadIcon()274 public Drawable loadIcon() { 275 return null; 276 } 277 278 @Override getKey()279 public String getKey() { 280 return mKey; 281 } 282 } 283 284 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 285 new BaseSearchIndexProvider(R.xml.screen_resolution_settings) { 286 @Override 287 protected boolean isPageSearchEnabled(Context context) { 288 ScreenResolutionController mController = 289 new ScreenResolutionController(context, "fragment"); 290 return mController.checkSupportedResolutions(); 291 } 292 }; 293 294 private static final class DisplayObserver implements DisplayManager.DisplayListener { 295 private final @Nullable Context mContext; 296 private int mDefaultDensity; 297 private int mCurrentIndex; 298 private AtomicInteger mPreviousWidth = new AtomicInteger(-1); 299 DisplayObserver(Context context)300 DisplayObserver(Context context) { 301 mContext = context; 302 } 303 startObserve()304 public void startObserve() { 305 if (mContext == null) { 306 return; 307 } 308 309 final DisplayDensityUtils density = new DisplayDensityUtils(mContext); 310 final int currentIndex = density.getCurrentIndexForDefaultDisplay(); 311 final int defaultDensity = density.getDefaultDensityForDefaultDisplay(); 312 313 if (density.getDefaultDisplayDensityValues()[mCurrentIndex] 314 == density.getDefaultDensityForDefaultDisplay()) { 315 return; 316 } 317 318 mDefaultDensity = defaultDensity; 319 mCurrentIndex = currentIndex; 320 final DisplayManager dm = mContext.getSystemService(DisplayManager.class); 321 dm.registerDisplayListener(this, null); 322 } 323 stopObserve()324 public void stopObserve() { 325 if (mContext == null) { 326 return; 327 } 328 329 final DisplayManager dm = mContext.getSystemService(DisplayManager.class); 330 dm.unregisterDisplayListener(this); 331 } 332 333 @Override onDisplayAdded(int displayId)334 public void onDisplayAdded(int displayId) {} 335 336 @Override onDisplayRemoved(int displayId)337 public void onDisplayRemoved(int displayId) {} 338 339 @Override onDisplayChanged(int displayId)340 public void onDisplayChanged(int displayId) { 341 if (displayId != Display.DEFAULT_DISPLAY) { 342 return; 343 } 344 345 if (!isDensityChanged() || !isResolutionChangeApplied()) { 346 return; 347 } 348 349 restoreDensity(); 350 stopObserve(); 351 } 352 restoreDensity()353 private void restoreDensity() { 354 final DisplayDensityUtils density = new DisplayDensityUtils(mContext); 355 if (density.getDefaultDisplayDensityValues()[mCurrentIndex] 356 != density.getDefaultDensityForDefaultDisplay()) { 357 density.setForcedDisplayDensity(mCurrentIndex); 358 } 359 360 mDefaultDensity = density.getDefaultDensityForDefaultDisplay(); 361 } 362 isDensityChanged()363 private boolean isDensityChanged() { 364 final DisplayDensityUtils density = new DisplayDensityUtils(mContext); 365 if (density.getDefaultDensityForDefaultDisplay() == mDefaultDensity) { 366 return false; 367 } 368 369 return true; 370 } 371 getCurrentWidth()372 private int getCurrentWidth() { 373 final DisplayManager dm = mContext.getSystemService(DisplayManager.class); 374 return dm.getDisplay(Display.DEFAULT_DISPLAY).getMode().getPhysicalWidth(); 375 } 376 setPendingResolutionChange(int selectedWidth)377 private boolean setPendingResolutionChange(int selectedWidth) { 378 int currentWidth = getCurrentWidth(); 379 380 if (selectedWidth == currentWidth) { 381 return false; 382 } 383 if (mPreviousWidth.get() != -1 && !isResolutionChangeApplied()) { 384 return false; 385 } 386 387 mPreviousWidth.set(currentWidth); 388 389 return true; 390 } 391 isResolutionChangeApplied()392 private boolean isResolutionChangeApplied() { 393 if (mPreviousWidth.get() == getCurrentWidth()) { 394 return false; 395 } 396 397 return true; 398 } 399 } 400 } 401