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 com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG; 20 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL; 21 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE; 22 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplaySizeSettingEnabled; 23 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isResolutionSettingEnabled; 24 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isRotationSettingEnabled; 25 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isTopologyPaneEnabled; 26 import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isUseDisplaySettingEnabled; 27 28 import android.app.Activity; 29 import android.app.settings.SettingsEnums; 30 import android.content.Context; 31 import android.os.Bundle; 32 import android.view.View; 33 import android.widget.TextView; 34 import android.window.DesktopExperienceFlags; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.preference.ListPreference; 39 import androidx.preference.Preference; 40 import androidx.preference.PreferenceCategory; 41 import androidx.preference.PreferenceGroup; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.settings.R; 45 import com.android.settings.SettingsPreferenceFragmentBase; 46 import com.android.settings.accessibility.TextReadingPreferenceFragment; 47 import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; 48 import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; 49 import com.android.settings.core.SubSettingLauncher; 50 import com.android.settingslib.widget.FooterPreference; 51 import com.android.settingslib.widget.IllustrationPreference; 52 import com.android.settingslib.widget.MainSwitchPreference; 53 54 import java.util.HashMap; 55 import java.util.List; 56 57 /** 58 * The Settings screen for External Displays configuration and connection management. 59 */ 60 public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase { 61 @VisibleForTesting enum PrefBasics { 62 DISPLAY_TOPOLOGY(10, "display_topology_preference", null), 63 MIRROR(20, "mirror_preference", R.string.external_display_mirroring_title), 64 65 // If shown, use toggle should be before other per-display settings. 66 EXTERNAL_DISPLAY_USE(30, "external_display_use_preference", 67 R.string.external_display_use_title), 68 69 ILLUSTRATION(35, "external_display_illustration", null), 70 71 // If shown, external display size is before other per-display settings. 72 EXTERNAL_DISPLAY_SIZE(40, "external_display_size", R.string.screen_zoom_title), 73 EXTERNAL_DISPLAY_ROTATION(50, "external_display_rotation", 74 R.string.external_display_rotation), 75 EXTERNAL_DISPLAY_RESOLUTION(60, "external_display_resolution", 76 R.string.external_display_resolution_settings_title), 77 78 // Built-in display link is before per-display settings. 79 BUILTIN_DISPLAY_LIST(70, "builtin_display_list_preference", 80 R.string.builtin_display_settings_category), 81 82 EXTERNAL_DISPLAY_LIST(-1, "external_display_list", null), 83 84 // If shown, footer should appear below everything. 85 FOOTER(90, "footer_preference", null); 86 87 PrefBasics(int order, String key, @Nullable Integer titleResource)88 PrefBasics(int order, String key, @Nullable Integer titleResource) { 89 this.order = order; 90 this.key = key; 91 this.titleResource = titleResource; 92 } 93 94 // Fields must be public to make the linter happy. 95 public final int order; 96 public final String key; 97 @Nullable public final Integer titleResource; 98 99 /** 100 * Applies this basic data to the given preference. 101 * 102 * @param preference object whose properties to set 103 * @param nth if non-null, disambiguates the key so that other preferences can have the same 104 * basic properties. Does not affect the order. 105 */ apply(Preference preference, @Nullable Integer nth)106 void apply(Preference preference, @Nullable Integer nth) { 107 if (order != -1) { 108 preference.setOrder(order); 109 } 110 if (titleResource != null) { 111 preference.setTitle(titleResource); 112 } 113 preference.setKey(nth == null ? key : keyForNth(nth)); 114 preference.setPersistent(false); 115 } 116 keyForNth(int nth)117 String keyForNth(int nth) { 118 return key + "_" + nth; 119 } 120 } 121 122 static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings; 123 static final int EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE = 124 R.string.external_display_change_resolution_footer_title; 125 static final int EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE = 126 R.drawable.external_display_mirror_landscape; 127 static final int EXTERNAL_DISPLAY_TITLE_RESOURCE = 128 R.string.external_display_settings_title; 129 static final int EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE = 130 R.string.external_display_not_found_footer_title; 131 static final int EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE = 132 R.drawable.external_display_mirror_portrait; 133 static final int EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE = R.string.screen_zoom_short_summary; 134 135 private boolean mStarted; 136 @Nullable 137 private Preference mDisplayTopologyPreference; 138 @Nullable 139 private PreferenceCategory mBuiltinDisplayPreference; 140 @Nullable 141 private Preference mBuiltinDisplaySizeAndTextPreference; 142 @Nullable 143 private Injector mInjector; 144 @Nullable 145 private String[] mRotationEntries; 146 @Nullable 147 private String[] mRotationEntriesValues; 148 @NonNull 149 private final Runnable mUpdateRunnable = this::update; 150 private final DisplayListener mListener = new DisplayListener() { 151 @Override 152 public void update(int displayId) { 153 scheduleUpdate(); 154 } 155 }; 156 ExternalDisplayPreferenceFragment()157 public ExternalDisplayPreferenceFragment() {} 158 159 @VisibleForTesting ExternalDisplayPreferenceFragment(@onNull Injector injector)160 ExternalDisplayPreferenceFragment(@NonNull Injector injector) { 161 mInjector = injector; 162 } 163 164 @Override getMetricsCategory()165 public int getMetricsCategory() { 166 return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; 167 } 168 169 @Override getHelpResource()170 public int getHelpResource() { 171 return EXTERNAL_DISPLAY_HELP_URL; 172 } 173 174 @Override onCreateCallback(@ullable Bundle icicle)175 public void onCreateCallback(@Nullable Bundle icicle) { 176 if (mInjector == null) { 177 mInjector = new Injector(getPrefContext()); 178 } 179 addPreferencesFromResource(EXTERNAL_DISPLAY_SETTINGS_RESOURCE); 180 } 181 182 @Override onActivityCreatedCallback(@ullable Bundle savedInstanceState)183 public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) { 184 View view = getView(); 185 TextView emptyView = null; 186 if (view != null) { 187 emptyView = view.findViewById(android.R.id.empty); 188 } 189 if (emptyView != null) { 190 emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE); 191 setEmptyView(emptyView); 192 } 193 } 194 195 @Override onStartCallback()196 public void onStartCallback() { 197 mStarted = true; 198 if (mInjector == null) { 199 return; 200 } 201 mInjector.registerDisplayListener(mListener); 202 scheduleUpdate(); 203 } 204 205 @Override onStopCallback()206 public void onStopCallback() { 207 mStarted = false; 208 if (mInjector == null) { 209 return; 210 } 211 mInjector.unregisterDisplayListener(mListener); 212 unscheduleUpdate(); 213 } 214 215 /** 216 * @return id of the preference. 217 */ 218 @Override getPreferenceScreenResId()219 protected int getPreferenceScreenResId() { 220 return EXTERNAL_DISPLAY_SETTINGS_RESOURCE; 221 } 222 223 @VisibleForTesting launchResolutionSelector(@onNull final Context context, final int displayId)224 protected void launchResolutionSelector(@NonNull final Context context, final int displayId) { 225 final Bundle args = new Bundle(); 226 args.putInt(DISPLAY_ID_ARG, displayId); 227 new SubSettingLauncher(context) 228 .setDestination(ResolutionPreferenceFragment.class.getName()) 229 .setArguments(args) 230 .setSourceMetricsCategory(getMetricsCategory()).launch(); 231 } 232 233 @VisibleForTesting launchBuiltinDisplaySettings()234 protected void launchBuiltinDisplaySettings() { 235 final Bundle args = new Bundle(); 236 var context = getPrefContext(); 237 new SubSettingLauncher(context) 238 .setDestination(TextReadingPreferenceFragment.class.getName()) 239 .setArguments(args) 240 .setSourceMetricsCategory(getMetricsCategory()).launch(); 241 } 242 243 // The real FooterPreference requires a resource which is not available in unit tests. 244 @VisibleForTesting newFooterPreference(Context context)245 Preference newFooterPreference(Context context) { 246 return new FooterPreference(context); 247 } 248 249 /** 250 * Returns the preference for the footer. 251 */ addFooterPreference(Context context, PrefRefresh refresh, int title)252 private void addFooterPreference(Context context, PrefRefresh refresh, int title) { 253 var pref = refresh.findUnusedPreference(PrefBasics.FOOTER.key); 254 if (pref == null) { 255 pref = newFooterPreference(context); 256 PrefBasics.FOOTER.apply(pref, /* nth= */ null); 257 } 258 pref.setTitle(title); 259 refresh.addPreference(pref); 260 } 261 262 @NonNull reuseRotationPreference(@onNull Context context, PrefRefresh refresh, int position)263 private ListPreference reuseRotationPreference(@NonNull Context context, PrefRefresh refresh, 264 int position) { 265 ListPreference pref = refresh.findUnusedPreference( 266 PrefBasics.EXTERNAL_DISPLAY_ROTATION.keyForNth(position)); 267 if (pref == null) { 268 pref = new ListPreference(context); 269 PrefBasics.EXTERNAL_DISPLAY_ROTATION.apply(pref, position); 270 } 271 refresh.addPreference(pref); 272 return pref; 273 } 274 275 @NonNull reuseResolutionPreference(@onNull Context context, PrefRefresh refresh, int position)276 private Preference reuseResolutionPreference(@NonNull Context context, PrefRefresh refresh, 277 int position) { 278 var pref = refresh.findUnusedPreference( 279 PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.keyForNth(position)); 280 if (pref == null) { 281 pref = new Preference(context); 282 PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.apply(pref, position); 283 } 284 refresh.addPreference(pref); 285 return pref; 286 } 287 288 @NonNull reuseUseDisplayPreference( Context context, PrefRefresh refresh, int position)289 private MainSwitchPreference reuseUseDisplayPreference( 290 Context context, PrefRefresh refresh, int position) { 291 MainSwitchPreference pref = refresh.findUnusedPreference( 292 PrefBasics.EXTERNAL_DISPLAY_USE.keyForNth(position)); 293 if (pref == null) { 294 pref = new MainSwitchPreference(context); 295 PrefBasics.EXTERNAL_DISPLAY_USE.apply(pref, position); 296 } 297 refresh.addPreference(pref); 298 return pref; 299 } 300 301 @NonNull reuseIllustrationPreference( Context context, PrefRefresh refresh)302 private IllustrationPreference reuseIllustrationPreference( 303 Context context, PrefRefresh refresh) { 304 IllustrationPreference pref = refresh.findUnusedPreference(PrefBasics.ILLUSTRATION.key); 305 if (pref == null) { 306 pref = new IllustrationPreference(context); 307 PrefBasics.ILLUSTRATION.apply(pref, /* nth= */ null); 308 } 309 refresh.addPreference(pref); 310 return pref; 311 } 312 313 @NonNull getBuiltinDisplayListPreference(@onNull Context context)314 private PreferenceCategory getBuiltinDisplayListPreference(@NonNull Context context) { 315 if (mBuiltinDisplayPreference == null) { 316 mBuiltinDisplayPreference = new PreferenceCategory(context); 317 PrefBasics.BUILTIN_DISPLAY_LIST.apply(mBuiltinDisplayPreference, /* nth= */ null); 318 } 319 return mBuiltinDisplayPreference; 320 } 321 322 @NonNull getBuiltinDisplaySizeAndTextPreference(@onNull Context context)323 private Preference getBuiltinDisplaySizeAndTextPreference(@NonNull Context context) { 324 if (mBuiltinDisplaySizeAndTextPreference == null) { 325 mBuiltinDisplaySizeAndTextPreference = new BuiltinDisplaySizeAndTextPreference(context); 326 } 327 return mBuiltinDisplaySizeAndTextPreference; 328 } 329 getDisplayTopologyPreference(@onNull Context context)330 @NonNull Preference getDisplayTopologyPreference(@NonNull Context context) { 331 if (mDisplayTopologyPreference == null) { 332 mDisplayTopologyPreference = new DisplayTopologyPreference(context); 333 PrefBasics.DISPLAY_TOPOLOGY.apply(mDisplayTopologyPreference, /* nth= */ null); 334 } 335 return mDisplayTopologyPreference; 336 } 337 addMirrorPreference(Context context, PrefRefresh refresh)338 private void addMirrorPreference(Context context, PrefRefresh refresh) { 339 Preference pref = refresh.findUnusedPreference(PrefBasics.MIRROR.key); 340 if (pref == null) { 341 pref = new MirrorPreference(context, 342 DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()); 343 PrefBasics.MIRROR.apply(pref, /* nth= */ null); 344 } 345 refresh.addPreference(pref); 346 } 347 348 @NonNull reuseSizePreference(Context context, PrefRefresh refresh, DisplayDevice display, int position)349 private ExternalDisplaySizePreference reuseSizePreference(Context context, 350 PrefRefresh refresh, DisplayDevice display, int position) { 351 ExternalDisplaySizePreference pref = 352 refresh.findUnusedPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.keyForNth(position)); 353 if (pref == null) { 354 pref = new ExternalDisplaySizePreference(context, /* attrs= */ null); 355 PrefBasics.EXTERNAL_DISPLAY_SIZE.apply(pref, position); 356 } 357 if (display.getMode() != null) { 358 pref.setStateForPreference(display.getMode().getPhysicalWidth(), 359 display.getMode().getPhysicalHeight(), display.getId()); 360 } 361 refresh.addPreference(pref); 362 return pref; 363 } 364 update()365 private void update() { 366 final var screen = getPreferenceScreen(); 367 if (screen == null || mInjector == null || mInjector.getContext() == null) { 368 return; 369 } 370 try (var cleanableScreen = new PrefRefresh(screen)) { 371 updateScreen(cleanableScreen, mInjector.getContext()); 372 } 373 } 374 updateScreen(final PrefRefresh screen, Context context)375 private void updateScreen(final PrefRefresh screen, Context context) { 376 final var displaysToShow = mInjector == null 377 ? List.<DisplayDevice>of() : mInjector.getConnectedDisplays(); 378 379 if (displaysToShow.isEmpty()) { 380 showTextWhenNoDisplaysToShow(screen, context, /* position= */ 0); 381 } else { 382 showDisplaysList(displaysToShow, screen, context); 383 } 384 385 final Activity activity = getCurrentActivity(); 386 if (activity != null) { 387 activity.setTitle(EXTERNAL_DISPLAY_TITLE_RESOURCE); 388 } 389 } 390 showTextWhenNoDisplaysToShow(@onNull final PrefRefresh screen, @NonNull Context context, int position)391 private void showTextWhenNoDisplaysToShow(@NonNull final PrefRefresh screen, 392 @NonNull Context context, int position) { 393 if (isUseDisplaySettingEnabled(mInjector)) { 394 addUseDisplayPreferenceNoDisplaysFound(context, screen, position); 395 } 396 addFooterPreference(context, screen, EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE); 397 } 398 reuseDisplayCategory( PrefRefresh screen, Context context, int position)399 private static PreferenceCategory reuseDisplayCategory( 400 PrefRefresh screen, Context context, int position) { 401 // The rest of the settings are in a category with the display name as the title. 402 String categoryKey = PrefBasics.EXTERNAL_DISPLAY_LIST.keyForNth(position); 403 var category = (PreferenceCategory) screen.findUnusedPreference(categoryKey); 404 405 if (category != null) { 406 screen.addPreference(category); 407 } else { 408 category = new PreferenceCategory(context); 409 screen.addPreference(category); 410 PrefBasics.EXTERNAL_DISPLAY_LIST.apply(category, position); 411 category.setOrder(PrefBasics.BUILTIN_DISPLAY_LIST.order + 1 + position); 412 } 413 414 return category; 415 } 416 showDisplaySettings(DisplayDevice display, PrefRefresh refresh, Context context, boolean includeV1Helpers, int position)417 private void showDisplaySettings(DisplayDevice display, PrefRefresh refresh, 418 Context context, boolean includeV1Helpers, int position) { 419 if (isUseDisplaySettingEnabled(mInjector)) { 420 addUseDisplayPreferenceForDisplay(context, refresh, display, position); 421 } 422 final var displayRotation = getDisplayRotation(display.getId()); 423 if (includeV1Helpers && display.isEnabled() == DisplayIsEnabled.YES) { 424 addIllustrationImage(context, refresh, displayRotation); 425 } 426 427 addResolutionPreference(context, refresh, display, position); 428 addRotationPreference(context, refresh, display, displayRotation, position); 429 if (isResolutionSettingEnabled(mInjector)) { 430 // Do not show the footer about changing resolution affecting apps. This is not in the 431 // UX design for v2, and there is no good place to put it, since (a) if it is on the 432 // bottom of the screen, the external resolution setting must be below the built-in 433 // display options for the per-display fragment, which is too hidden for the per-display 434 // fragment, or (b) the footer is above the Built-in display settings, rather than the 435 // bottom of the screen, which contradicts the visual style and purpose of the 436 // FooterPreference class, or (c) we must hide the built-in display settings, which is 437 // inconsistent with the topology pane, which shows that display. 438 // TODO(b/352648432): probably remove footer once the pane and rest of v2 UI is in 439 // place. 440 if (includeV1Helpers && display.isEnabled() == DisplayIsEnabled.YES) { 441 addFooterPreference( 442 context, refresh, EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); 443 } 444 } 445 if (isDisplaySizeSettingEnabled(mInjector)) { 446 addSizePreference(context, refresh, display, position); 447 } 448 } 449 maybeAddV2Components(Context context, PrefRefresh screen)450 private void maybeAddV2Components(Context context, PrefRefresh screen) { 451 if (isTopologyPaneEnabled(mInjector)) { 452 screen.addPreference(getDisplayTopologyPreference(context)); 453 addMirrorPreference(context, screen); 454 455 // If topology is shown, we also show a preference for the built-in display for 456 // consistency with the topology. 457 var builtinCategory = getBuiltinDisplayListPreference(context); 458 screen.addPreference(builtinCategory); 459 builtinCategory.addPreference(getBuiltinDisplaySizeAndTextPreference(context)); 460 } 461 } 462 showDisplaysList(@onNull List<DisplayDevice> displaysToShow, @NonNull PrefRefresh screen, @NonNull Context context)463 private void showDisplaysList(@NonNull List<DisplayDevice> displaysToShow, 464 @NonNull PrefRefresh screen, @NonNull Context context) { 465 maybeAddV2Components(context, screen); 466 int position = 0; 467 boolean includeV1Helpers = !isTopologyPaneEnabled(mInjector) && displaysToShow.size() <= 1; 468 for (var display : displaysToShow) { 469 var category = reuseDisplayCategory(screen, context, position); 470 category.setTitle(display.getName()); 471 472 try (var refresh = new PrefRefresh(category)) { 473 // The category may have already been populated if it was retrieved from `screen`, 474 // but we still need to update resolution and rotation items. 475 showDisplaySettings(display, refresh, context, includeV1Helpers, position); 476 } 477 478 position++; 479 } 480 } 481 addUseDisplayPreferenceNoDisplaysFound(Context context, PrefRefresh refresh, int position)482 private void addUseDisplayPreferenceNoDisplaysFound(Context context, PrefRefresh refresh, 483 int position) { 484 final var pref = reuseUseDisplayPreference(context, refresh, position); 485 pref.setChecked(false); 486 pref.setEnabled(false); 487 pref.setOnPreferenceChangeListener(null); 488 } 489 addUseDisplayPreferenceForDisplay(final Context context, PrefRefresh refresh, final DisplayDevice display, int position)490 private void addUseDisplayPreferenceForDisplay(final Context context, 491 PrefRefresh refresh, final DisplayDevice display, int position) { 492 final var pref = reuseUseDisplayPreference(context, refresh, position); 493 pref.setChecked(display.isEnabled() == DisplayIsEnabled.YES); 494 pref.setEnabled(true); 495 pref.setOnPreferenceChangeListener((p, newValue) -> { 496 writePreferenceClickMetric(p); 497 final boolean result; 498 if (mInjector == null) { 499 return false; 500 } 501 if ((Boolean) newValue) { 502 result = mInjector.enableConnectedDisplay(display.getId()); 503 } else { 504 result = mInjector.disableConnectedDisplay(display.getId()); 505 } 506 if (result) { 507 pref.setChecked((Boolean) newValue); 508 } 509 return result; 510 }); 511 } 512 addIllustrationImage(final Context context, PrefRefresh refresh, final int displayRotation)513 private void addIllustrationImage(final Context context, PrefRefresh refresh, 514 final int displayRotation) { 515 var pref = reuseIllustrationPreference(context, refresh); 516 if (displayRotation % 2 == 0) { 517 pref.setLottieAnimationResId(EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE); 518 } else { 519 pref.setLottieAnimationResId(EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE); 520 } 521 } 522 addRotationPreference(final Context context, PrefRefresh refresh, final DisplayDevice display, final int displayRotation, int position)523 private void addRotationPreference(final Context context, PrefRefresh refresh, 524 final DisplayDevice display, final int displayRotation, int position) { 525 var pref = reuseRotationPreference(context, refresh, position); 526 if (mRotationEntries == null || mRotationEntriesValues == null) { 527 mRotationEntries = new String[] { 528 context.getString(R.string.external_display_standard_rotation), 529 context.getString(R.string.external_display_rotation_90), 530 context.getString(R.string.external_display_rotation_180), 531 context.getString(R.string.external_display_rotation_270)}; 532 mRotationEntriesValues = new String[] {"0", "1", "2", "3"}; 533 } 534 pref.setEntries(mRotationEntries); 535 pref.setEntryValues(mRotationEntriesValues); 536 pref.setValueIndex(displayRotation); 537 pref.setSummary(mRotationEntries[displayRotation]); 538 pref.setOnPreferenceChangeListener((p, newValue) -> { 539 writePreferenceClickMetric(p); 540 var rotation = Integer.parseInt((String) newValue); 541 var displayId = display.getId(); 542 if (mInjector == null || !mInjector.freezeDisplayRotation(displayId, rotation)) { 543 return false; 544 } 545 pref.setValueIndex(rotation); 546 return true; 547 }); 548 pref.setEnabled(display.isEnabled() == DisplayIsEnabled.YES 549 && isRotationSettingEnabled(mInjector)); 550 } 551 addResolutionPreference(final Context context, PrefRefresh refresh, final DisplayDevice display, int position)552 private void addResolutionPreference(final Context context, PrefRefresh refresh, 553 final DisplayDevice display, int position) { 554 var pref = reuseResolutionPreference(context, refresh, position); 555 pref.setSummary(display.getMode().getPhysicalWidth() + " x " 556 + display.getMode().getPhysicalHeight()); 557 pref.setOnPreferenceClickListener((Preference p) -> { 558 writePreferenceClickMetric(p); 559 launchResolutionSelector(context, display.getId()); 560 return true; 561 }); 562 pref.setEnabled(display.isEnabled() == DisplayIsEnabled.YES 563 && isResolutionSettingEnabled(mInjector)); 564 } 565 addSizePreference(final Context context, PrefRefresh refresh, DisplayDevice display, int position)566 private void addSizePreference(final Context context, PrefRefresh refresh, 567 DisplayDevice display, int position) { 568 var pref = reuseSizePreference(context, refresh, display, position); 569 pref.setSummary(EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE); 570 pref.setOnPreferenceClickListener( 571 (Preference p) -> { 572 writePreferenceClickMetric(p); 573 return true; 574 }); 575 pref.setEnabled(display.isEnabled() == DisplayIsEnabled.YES); 576 } 577 getDisplayRotation(int displayId)578 private int getDisplayRotation(int displayId) { 579 if (mInjector == null) { 580 return 0; 581 } 582 return Math.min(3, Math.max(0, mInjector.getDisplayUserRotation(displayId))); 583 } 584 scheduleUpdate()585 private void scheduleUpdate() { 586 if (mInjector == null || !mStarted) { 587 return; 588 } 589 unscheduleUpdate(); 590 mInjector.getHandler().post(mUpdateRunnable); 591 } 592 unscheduleUpdate()593 private void unscheduleUpdate() { 594 if (mInjector == null || !mStarted) { 595 return; 596 } 597 mInjector.getHandler().removeCallbacks(mUpdateRunnable); 598 } 599 600 private class BuiltinDisplaySizeAndTextPreference extends Preference 601 implements Preference.OnPreferenceClickListener { BuiltinDisplaySizeAndTextPreference(@onNull final Context context)602 BuiltinDisplaySizeAndTextPreference(@NonNull final Context context) { 603 super(context); 604 605 setPersistent(false); 606 setKey("builtin_display_size_and_text"); 607 setTitle(R.string.accessibility_text_reading_options_title); 608 setOnPreferenceClickListener(this); 609 } 610 611 @Override onPreferenceClick(@onNull Preference preference)612 public boolean onPreferenceClick(@NonNull Preference preference) { 613 launchBuiltinDisplaySettings(); 614 return true; 615 } 616 } 617 618 private static class PrefRefresh implements AutoCloseable { 619 private final PreferenceGroup mScreen; 620 private final HashMap<String, Preference> mUnusedPreferences = new HashMap<>(); 621 PrefRefresh(@onNull final PreferenceGroup screen)622 PrefRefresh(@NonNull final PreferenceGroup screen) { 623 mScreen = screen; 624 int preferencesCount = mScreen.getPreferenceCount(); 625 for (int i = 0; i < preferencesCount; i++) { 626 var pref = mScreen.getPreference(i); 627 if (pref.hasKey()) { 628 mUnusedPreferences.put(pref.getKey(), pref); 629 } 630 } 631 } 632 633 @Nullable findUnusedPreference(@onNull String key)634 <P extends Preference> P findUnusedPreference(@NonNull String key) { 635 return (P) mUnusedPreferences.get(key); 636 } 637 addPreference(@onNull final Preference pref)638 boolean addPreference(@NonNull final Preference pref) { 639 if (pref.hasKey()) { 640 final var previousPref = mUnusedPreferences.get(pref.getKey()); 641 if (pref == previousPref) { 642 // Exact preference already added, no need to add it again. 643 // And no need to remove this preference either. 644 mUnusedPreferences.remove(pref.getKey()); 645 return true; 646 } 647 // Exact preference is not yet added 648 } 649 return mScreen.addPreference(pref); 650 } 651 652 @Override close()653 public void close() { 654 for (var v : mUnusedPreferences.values()) { 655 mScreen.removePreference(v); 656 } 657 } 658 } 659 } 660