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