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