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