• 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 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