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.accessibility;
18 
19 import static com.android.internal.accessibility.AccessibilityShortcutController.FONT_SIZE_COMPONENT_NAME;
20 import static com.android.settings.accessibility.TextReadingResetController.ResetStateListener;
21 
22 import android.app.Activity;
23 import android.app.Dialog;
24 import android.app.settings.SettingsEnums;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.os.Bundle;
29 import android.view.View;
30 import android.widget.Toast;
31 
32 import androidx.annotation.IntDef;
33 import androidx.appcompat.app.AlertDialog;
34 
35 import com.android.settings.R;
36 import com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums;
37 import com.android.settings.dashboard.DashboardFragment;
38 import com.android.settings.search.BaseSearchIndexProvider;
39 import com.android.settingslib.core.AbstractPreferenceController;
40 import com.android.settingslib.search.SearchIndexable;
41 
42 import com.google.android.setupcompat.util.WizardManagerHelper;
43 import com.google.common.annotations.VisibleForTesting;
44 
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.stream.Collectors;
50 
51 /**
52  * Accessibility settings for adjusting the system features which are related to the reading. For
53  * example, bold text, high contrast text, display size, font size and so on.
54  */
55 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
56 public class TextReadingPreferenceFragment extends DashboardFragment {
57     public static final String EXTRA_LAUNCHED_FROM = "launched_from";
58     private static final String TAG = "TextReadingPreferenceFragment";
59     private static final String SETUP_WIZARD_PACKAGE = "setupwizard";
60     static final String FONT_SIZE_KEY = "font_size";
61     static final String DISPLAY_SIZE_KEY = "display_size";
62     static final String BOLD_TEXT_KEY = "toggle_force_bold_text";
63     static final String HIGH_TEXT_CONTRAST_KEY = "toggle_high_text_contrast_preference";
64     static final String RESET_KEY = "reset";
65     static final String PREVIEW_KEY = "preview";
66     private static final String NEED_RESET_SETTINGS = "need_reset_settings";
67     private static final String LAST_PREVIEW_INDEX = "last_preview_index";
68     private static final int UNKNOWN_INDEX = -1;
69 
70     private FontWeightAdjustmentPreferenceController mFontWeightAdjustmentController;
71     private TextReadingPreviewController mPreviewController;
72     private int mEntryPoint = EntryPoint.UNKNOWN_ENTRY;
73 
74     /**
75      * The entry point which launches the {@link TextReadingPreferenceFragment}.
76      *
77      * <p>This should only be used for logging.
78      */
79     @Retention(RetentionPolicy.SOURCE)
80     @IntDef({
81             EntryPoint.UNKNOWN_ENTRY,
82             EntryPoint.SUW_VISION_SETTINGS,
83             EntryPoint.SUW_ANYTHING_ELSE,
84             EntryPoint.DISPLAY_SETTINGS,
85             EntryPoint.ACCESSIBILITY_SETTINGS,
86     })
87     @interface EntryPoint {
88         int UNKNOWN_ENTRY = 0;
89         int SUW_VISION_SETTINGS = 1;
90         int SUW_ANYTHING_ELSE = 2;
91         int DISPLAY_SETTINGS = 3;
92         int ACCESSIBILITY_SETTINGS = 4;
93     }
94 
95     @VisibleForTesting
96     List<ResetStateListener> mResetStateListeners;
97 
98     @VisibleForTesting
99     boolean mNeedResetSettings;
100 
101     @Override
onCreate(Bundle savedInstanceState)102     public void onCreate(Bundle savedInstanceState) {
103         super.onCreate(savedInstanceState);
104 
105         mNeedResetSettings = false;
106         mResetStateListeners = getResetStateListeners();
107 
108         if (savedInstanceState != null) {
109             if (savedInstanceState.getBoolean(NEED_RESET_SETTINGS)) {
110                 mResetStateListeners.forEach(ResetStateListener::resetState);
111             }
112 
113             if (savedInstanceState.containsKey(LAST_PREVIEW_INDEX)) {
114                 final int lastPreviewIndex = savedInstanceState.getInt(LAST_PREVIEW_INDEX);
115                 if (lastPreviewIndex != UNKNOWN_INDEX) {
116                     mPreviewController.setCurrentItem(lastPreviewIndex);
117                 }
118             }
119         }
120     }
121 
122     @Override
onActivityCreated(Bundle savedInstanceState)123     public void onActivityCreated(Bundle savedInstanceState) {
124         super.onActivityCreated(savedInstanceState);
125         final View rootView = getActivity().getWindow().peekDecorView();
126         if (rootView != null) {
127             rootView.setAccessibilityPaneTitle(getString(
128                     R.string.accessibility_text_reading_options_title));
129         }
130     }
131 
132     @Override
getPreferenceScreenResId()133     protected int getPreferenceScreenResId() {
134         return R.xml.accessibility_text_reading_options;
135     }
136 
137     @Override
getLogTag()138     protected String getLogTag() {
139         return TAG;
140     }
141 
142     @Override
getMetricsCategory()143     public int getMetricsCategory() {
144         return SettingsEnums.ACCESSIBILITY_TEXT_READING_OPTIONS;
145     }
146 
147     @Override
createPreferenceControllers(Context context)148     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
149         updateEntryPoint();
150 
151         final List<AbstractPreferenceController> controllers = new ArrayList<>();
152         final FontSizeData fontSizeData = new FontSizeData(context);
153         final DisplaySizeData displaySizeData = createDisplaySizeData(context);
154 
155         mPreviewController = new TextReadingPreviewController(context, PREVIEW_KEY, fontSizeData,
156                 displaySizeData);
157         mPreviewController.setEntryPoint(mEntryPoint);
158         controllers.add(mPreviewController);
159 
160         final PreviewSizeSeekBarController fontSizeController = new PreviewSizeSeekBarController(
161                 context, FONT_SIZE_KEY, fontSizeData) {
162             @Override
163             ComponentName getTileComponentName() {
164                 return FONT_SIZE_COMPONENT_NAME;
165             }
166 
167             @Override
168             CharSequence getTileTooltipContent() {
169                 return context.getText(
170                         R.string.accessibility_font_scaling_auto_added_qs_tooltip_content);
171             }
172         };
173         final String[] labelArray = new String[fontSizeData.getValues().size()];
174         for (int i = 0; i < labelArray.length; i++) {
175             labelArray[i] =
176                     context.getResources().getString(
177                             com.android.settingslib.R.string.font_scale_percentage,
178                             (int) (fontSizeData.getValues().get(i) * 100)
179                     );
180         }
181         fontSizeController.setProgressStateLabels(labelArray);
182         fontSizeController.setInteractionListener(mPreviewController);
183         getSettingsLifecycle().addObserver(fontSizeController);
184         controllers.add(fontSizeController);
185 
186         final PreviewSizeSeekBarController displaySizeController = new PreviewSizeSeekBarController(
187                 context, DISPLAY_SIZE_KEY, displaySizeData) {
188             @Override
189             ComponentName getTileComponentName() {
190                 return null;
191             }
192 
193             @Override
194             CharSequence getTileTooltipContent() {
195                 return null;
196             }
197         };
198         displaySizeController.setInteractionListener(mPreviewController);
199         controllers.add(displaySizeController);
200 
201         mFontWeightAdjustmentController =
202                 new FontWeightAdjustmentPreferenceController(context, BOLD_TEXT_KEY);
203         mFontWeightAdjustmentController.setEntryPoint(mEntryPoint);
204         controllers.add(mFontWeightAdjustmentController);
205 
206         final HighTextContrastPreferenceController highTextContrastController =
207                 new HighTextContrastPreferenceController(context, HIGH_TEXT_CONTRAST_KEY);
208         highTextContrastController.setEntryPoint(mEntryPoint);
209         controllers.add(highTextContrastController);
210 
211         final TextReadingResetController resetController =
212                 new TextReadingResetController(context, RESET_KEY,
213                         v -> showDialog(DialogEnums.DIALOG_RESET_SETTINGS));
214         resetController.setEntryPoint(mEntryPoint);
215         resetController.setVisible(!WizardManagerHelper.isAnySetupWizard(getIntent()));
216         controllers.add(resetController);
217 
218         return controllers;
219     }
220 
221     @Override
onCreateDialog(int dialogId)222     public Dialog onCreateDialog(int dialogId) {
223         if (dialogId == DialogEnums.DIALOG_RESET_SETTINGS) {
224             return new AlertDialog.Builder(getPrefContext())
225                     .setTitle(R.string.accessibility_text_reading_confirm_dialog_title)
226                     .setMessage(R.string.accessibility_text_reading_confirm_dialog_message)
227                     .setPositiveButton(
228                             R.string.accessibility_text_reading_confirm_dialog_reset_button,
229                             this::onPositiveButtonClicked)
230                     .setNegativeButton(R.string.cancel, /* listener= */ null)
231                     .create();
232         }
233 
234         throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
235     }
236 
237     @Override
getDialogMetricsCategory(int dialogId)238     public int getDialogMetricsCategory(int dialogId) {
239         if (dialogId == DialogEnums.DIALOG_RESET_SETTINGS) {
240             return SettingsEnums.DIALOG_RESET_SETTINGS;
241         }
242 
243         return super.getDialogMetricsCategory(dialogId);
244     }
245 
246     @Override
onSaveInstanceState(Bundle outState)247     public void onSaveInstanceState(Bundle outState) {
248         super.onSaveInstanceState(outState);
249 
250         if (mNeedResetSettings) {
251             outState.putBoolean(NEED_RESET_SETTINGS, true);
252         }
253 
254         outState.putInt(LAST_PREVIEW_INDEX, mPreviewController.getCurrentItem());
255     }
256 
257     @Override
onStart()258     public void onStart() {
259         super.onStart();
260     }
261 
isCallingFromAnythingElseEntryPoint()262     protected boolean isCallingFromAnythingElseEntryPoint() {
263         final Activity activity = getActivity();
264         final String callingPackage = activity != null ? activity.getCallingPackage() : null;
265 
266         return callingPackage != null && callingPackage.contains(SETUP_WIZARD_PACKAGE);
267     }
268 
269     @VisibleForTesting
createDisplaySizeData(Context context)270     DisplaySizeData createDisplaySizeData(Context context) {
271         return new DisplaySizeData(context);
272     }
273 
updateEntryPoint()274     private void updateEntryPoint() {
275         final Bundle bundle = getArguments();
276         if (bundle != null && bundle.containsKey(EXTRA_LAUNCHED_FROM)) {
277             mEntryPoint = bundle.getInt(EXTRA_LAUNCHED_FROM, EntryPoint.UNKNOWN_ENTRY);
278             return;
279         }
280 
281         mEntryPoint = isCallingFromAnythingElseEntryPoint()
282                 ? EntryPoint.SUW_ANYTHING_ELSE : EntryPoint.UNKNOWN_ENTRY;
283     }
284 
onPositiveButtonClicked(DialogInterface dialog, int which)285     private void onPositiveButtonClicked(DialogInterface dialog, int which) {
286         // To avoid showing the dialog again, probably the onDetach() of SettingsDialogFragment
287         // was interrupted by unexpectedly recreating the activity.
288         removeDialog(DialogEnums.DIALOG_RESET_SETTINGS);
289 
290         if (mFontWeightAdjustmentController.isChecked()) {
291             // TODO(b/228956791): Consider replacing or removing it once the root cause is
292             //  clarified and the better method is available.
293             // Probably has the race condition issue between "Bold text" and  the other features
294             // including "Display Size", “Font Size” if they would be enabled at the same time,
295             // so our workaround is that the “Bold text” would be reset first and then do the
296             // remaining to avoid flickering problem.
297             mNeedResetSettings = true;
298             mFontWeightAdjustmentController.resetState();
299         } else {
300             mResetStateListeners.forEach(ResetStateListener::resetState);
301         }
302 
303         Toast.makeText(getPrefContext(), R.string.accessibility_text_reading_reset_message,
304                 Toast.LENGTH_SHORT).show();
305     }
306 
getResetStateListeners()307     private List<ResetStateListener> getResetStateListeners() {
308         final List<AbstractPreferenceController> controllers = new ArrayList<>();
309         getPreferenceControllers().forEach(controllers::addAll);
310         return controllers.stream().filter(c -> c instanceof ResetStateListener).map(
311                 c -> (ResetStateListener) c).collect(Collectors.toList());
312     }
313 
314     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
315             new BaseSearchIndexProvider(R.xml.accessibility_text_reading_options);
316 }
317