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