1 /* 2 * Copyright (C) 2019 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.settings.accessibility.AccessibilityUtil.State.OFF; 20 import static com.android.settings.accessibility.AccessibilityUtil.State.ON; 21 22 import android.app.Dialog; 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.os.Bundle; 27 import android.provider.Settings; 28 import android.text.method.LinkMovementMethod; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.widget.AdapterView; 33 import android.widget.ListView; 34 import android.widget.TextView; 35 36 import androidx.annotation.DrawableRes; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.VisibleForTesting; 40 import androidx.preference.Preference; 41 import androidx.preference.PreferenceScreen; 42 43 import com.android.settings.DialogCreatable; 44 import com.android.settings.R; 45 import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; 46 import com.android.settings.core.BasePreferenceController; 47 import com.android.settings.utils.AnnotationSpan; 48 import com.android.settingslib.core.lifecycle.LifecycleObserver; 49 import com.android.settingslib.core.lifecycle.events.OnCreate; 50 import com.android.settingslib.core.lifecycle.events.OnResume; 51 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 56 /** Controller that shows the magnification area mode summary and the preference click behavior. */ 57 public class MagnificationModePreferenceController extends BasePreferenceController implements 58 DialogCreatable, LifecycleObserver, OnCreate, OnResume, OnSaveInstanceState { 59 60 static final String PREF_KEY = "screen_magnification_mode"; 61 private static final int DIALOG_ID_BASE = 10; 62 @VisibleForTesting 63 static final int DIALOG_MAGNIFICATION_MODE = DIALOG_ID_BASE + 1; 64 @VisibleForTesting 65 static final int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = DIALOG_ID_BASE + 2; 66 @VisibleForTesting 67 static final String EXTRA_MODE = "mode"; 68 69 private static final String TAG = "MagnificationModePreferenceController"; 70 71 private DialogHelper mDialogHelper; 72 // The magnification mode in the dialog. 73 @MagnificationMode 74 private int mModeCache = MagnificationMode.NONE; 75 private Preference mModePreference; 76 private ShortcutPreference mLinkPreference; 77 78 @VisibleForTesting 79 ListView mMagnificationModesListView; 80 81 private final List<MagnificationModeInfo> mModeInfos = new ArrayList<>(); 82 MagnificationModePreferenceController(Context context, String preferenceKey)83 public MagnificationModePreferenceController(Context context, String preferenceKey) { 84 super(context, preferenceKey); 85 initModeInfos(); 86 } 87 initModeInfos()88 private void initModeInfos() { 89 mModeInfos.add(new MagnificationModeInfo(mContext.getText( 90 R.string.accessibility_magnification_mode_dialog_option_full_screen), null, 91 R.drawable.a11y_magnification_mode_fullscreen, MagnificationMode.FULLSCREEN)); 92 mModeInfos.add(new MagnificationModeInfo( 93 mContext.getText(R.string.accessibility_magnification_mode_dialog_option_window), 94 null, R.drawable.a11y_magnification_mode_window, MagnificationMode.WINDOW)); 95 mModeInfos.add(new MagnificationModeInfo( 96 mContext.getText(R.string.accessibility_magnification_mode_dialog_option_switch), 97 mContext.getText( 98 R.string.accessibility_magnification_area_settings_mode_switch_summary), 99 R.drawable.a11y_magnification_mode_switch, MagnificationMode.ALL)); 100 } 101 102 @Override getAvailabilityStatus()103 public int getAvailabilityStatus() { 104 return AVAILABLE; 105 } 106 107 @Override getSummary()108 public CharSequence getSummary() { 109 final int capabilities = MagnificationCapabilities.getCapabilities(mContext); 110 return MagnificationCapabilities.getSummary(mContext, capabilities); 111 } 112 113 @Override onCreate(Bundle savedInstanceState)114 public void onCreate(Bundle savedInstanceState) { 115 if (savedInstanceState != null) { 116 mModeCache = savedInstanceState.getInt(EXTRA_MODE, MagnificationMode.NONE); 117 } 118 } 119 120 @Override displayPreference(PreferenceScreen screen)121 public void displayPreference(PreferenceScreen screen) { 122 super.displayPreference(screen); 123 mModePreference = screen.findPreference(getPreferenceKey()); 124 mLinkPreference = screen.findPreference( 125 ToggleFeaturePreferenceFragment.KEY_SHORTCUT_PREFERENCE); 126 mModePreference.setOnPreferenceClickListener(preference -> { 127 mModeCache = MagnificationCapabilities.getCapabilities(mContext); 128 mDialogHelper.showDialog(DIALOG_MAGNIFICATION_MODE); 129 return true; 130 }); 131 } 132 133 @Override onSaveInstanceState(Bundle outState)134 public void onSaveInstanceState(Bundle outState) { 135 outState.putInt(EXTRA_MODE, mModeCache); 136 } 137 138 /** 139 * Sets {@link DialogHelper} used to show the dialog. 140 */ setDialogHelper(DialogHelper dialogHelper)141 public void setDialogHelper(DialogHelper dialogHelper) { 142 mDialogHelper = dialogHelper; 143 mDialogHelper.setDialogDelegate(this); 144 } 145 146 @Override onCreateDialog(int dialogId)147 public Dialog onCreateDialog(int dialogId) { 148 switch (dialogId) { 149 case DIALOG_MAGNIFICATION_MODE: 150 return createMagnificationModeDialog(); 151 152 case DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING: 153 return createMagnificationTripleTapWarningDialog(); 154 } 155 return null; 156 } 157 158 @Override getDialogMetricsCategory(int dialogId)159 public int getDialogMetricsCategory(int dialogId) { 160 switch (dialogId) { 161 case DIALOG_MAGNIFICATION_MODE: 162 return SettingsEnums.DIALOG_MAGNIFICATION_CAPABILITY; 163 case DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING: 164 return SettingsEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING; 165 default: 166 return 0; 167 } 168 } 169 createMagnificationModeDialog()170 private Dialog createMagnificationModeDialog() { 171 mMagnificationModesListView = AccessibilityDialogUtils.createSingleChoiceListView( 172 mContext, mModeInfos, this::onMagnificationModeSelected); 173 174 final View headerView = LayoutInflater.from(mContext).inflate( 175 R.layout.accessibility_magnification_mode_header, mMagnificationModesListView, 176 false); 177 mMagnificationModesListView.addHeaderView(headerView, /* data= */ null, /* isSelectable= */ 178 false); 179 180 mMagnificationModesListView.setItemChecked(computeSelectionIndex(), true); 181 final CharSequence title = mContext.getString( 182 R.string.accessibility_magnification_mode_dialog_title); 183 final CharSequence positiveBtnText = mContext.getString(R.string.save); 184 final CharSequence negativeBtnText = mContext.getString(R.string.cancel); 185 186 return AccessibilityDialogUtils.createCustomDialog(mContext, title, 187 mMagnificationModesListView, 188 positiveBtnText, this::onMagnificationModeDialogPositiveButtonClicked, 189 negativeBtnText, /* negativeListener= */ null); 190 } 191 192 @VisibleForTesting onMagnificationModeDialogPositiveButtonClicked(DialogInterface dialogInterface, int which)193 void onMagnificationModeDialogPositiveButtonClicked(DialogInterface dialogInterface, 194 int which) { 195 final int selectedIndex = mMagnificationModesListView.getCheckedItemPosition(); 196 if (selectedIndex == AdapterView.INVALID_POSITION) { 197 Log.w(TAG, "invalid index"); 198 return; 199 } 200 201 mModeCache = ((MagnificationModeInfo) mMagnificationModesListView.getItemAtPosition( 202 selectedIndex)).mMagnificationMode; 203 204 // Do not save mode until user clicks positive button in triple tap warning dialog. 205 if (isTripleTapEnabled(mContext) && mModeCache != MagnificationMode.FULLSCREEN) { 206 mDialogHelper.showDialog(DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING); 207 } else { // Save mode (capabilities) value, don't need to show dialog to confirm. 208 updateCapabilitiesAndSummary(mModeCache); 209 } 210 } 211 updateCapabilitiesAndSummary(@agnificationMode int mode)212 private void updateCapabilitiesAndSummary(@MagnificationMode int mode) { 213 mModeCache = mode; 214 MagnificationCapabilities.setCapabilities(mContext, mModeCache); 215 mModePreference.setSummary( 216 MagnificationCapabilities.getSummary(mContext, mModeCache)); 217 } 218 onMagnificationModeSelected(AdapterView<?> parent, View view, int position, long id)219 private void onMagnificationModeSelected(AdapterView<?> parent, View view, int position, 220 long id) { 221 final MagnificationModeInfo modeInfo = 222 (MagnificationModeInfo) mMagnificationModesListView.getItemAtPosition( 223 position); 224 if (modeInfo.mMagnificationMode == mModeCache) { 225 return; 226 } 227 mModeCache = modeInfo.mMagnificationMode; 228 } 229 computeSelectionIndex()230 private int computeSelectionIndex() { 231 final int modesSize = mModeInfos.size(); 232 for (int i = 0; i < modesSize; i++) { 233 if (mModeInfos.get(i).mMagnificationMode == mModeCache) { 234 return i + mMagnificationModesListView.getHeaderViewsCount(); 235 } 236 } 237 Log.w(TAG, "computeSelectionIndex failed"); 238 return 0; 239 } 240 241 @VisibleForTesting isTripleTapEnabled(Context context)242 static boolean isTripleTapEnabled(Context context) { 243 return Settings.Secure.getInt(context.getContentResolver(), 244 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, OFF) == ON; 245 } 246 createMagnificationTripleTapWarningDialog()247 private Dialog createMagnificationTripleTapWarningDialog() { 248 final View contentView = LayoutInflater.from(mContext).inflate( 249 R.layout.magnification_triple_tap_warning_dialog, /* root= */ null); 250 final CharSequence title = mContext.getString( 251 R.string.accessibility_magnification_triple_tap_warning_title); 252 final CharSequence positiveBtnText = mContext.getString( 253 R.string.accessibility_magnification_triple_tap_warning_positive_button); 254 final CharSequence negativeBtnText = mContext.getString( 255 R.string.accessibility_magnification_triple_tap_warning_negative_button); 256 257 final Dialog dialog = AccessibilityDialogUtils.createCustomDialog(mContext, title, 258 contentView, 259 positiveBtnText, this::onMagnificationTripleTapWarningDialogPositiveButtonClicked, 260 negativeBtnText, this::onMagnificationTripleTapWarningDialogNegativeButtonClicked); 261 262 updateLinkInTripleTapWarningDialog(dialog, contentView); 263 264 return dialog; 265 } 266 updateLinkInTripleTapWarningDialog(Dialog dialog, View contentView)267 private void updateLinkInTripleTapWarningDialog(Dialog dialog, View contentView) { 268 final TextView messageView = contentView.findViewById(R.id.message); 269 // TODO(b/225682559): Need to remove performClick() after refactoring accessibility dialog. 270 final View.OnClickListener linkListener = view -> { 271 updateCapabilitiesAndSummary(mModeCache); 272 mLinkPreference.performClick(); 273 dialog.dismiss(); 274 }; 275 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( 276 AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener); 277 final CharSequence textWithLink = AnnotationSpan.linkify(mContext.getText( 278 R.string.accessibility_magnification_triple_tap_warning_message), linkInfo); 279 280 if (messageView != null) { 281 messageView.setText(textWithLink); 282 messageView.setMovementMethod(LinkMovementMethod.getInstance()); 283 } 284 dialog.setContentView(contentView); 285 } 286 287 @VisibleForTesting onMagnificationTripleTapWarningDialogNegativeButtonClicked( DialogInterface dialogInterface, int which)288 void onMagnificationTripleTapWarningDialogNegativeButtonClicked( 289 DialogInterface dialogInterface, int which) { 290 mModeCache = MagnificationCapabilities.getCapabilities(mContext); 291 mDialogHelper.showDialog(DIALOG_MAGNIFICATION_MODE); 292 } 293 294 @VisibleForTesting onMagnificationTripleTapWarningDialogPositiveButtonClicked( DialogInterface dialogInterface, int which)295 void onMagnificationTripleTapWarningDialogPositiveButtonClicked( 296 DialogInterface dialogInterface, int which) { 297 updateCapabilitiesAndSummary(mModeCache); 298 } 299 300 // TODO(b/186731461): Remove it when this controller is used in DashBoardFragment only. 301 @Override onResume()302 public void onResume() { 303 updateState(mModePreference); 304 } 305 306 /** 307 * An interface to help the delegate to show the dialog. It will be injected to the delegate. 308 */ 309 interface DialogHelper extends DialogCreatable { showDialog(int dialogId)310 void showDialog(int dialogId); setDialogDelegate(DialogCreatable delegate)311 void setDialogDelegate(DialogCreatable delegate); 312 } 313 314 @VisibleForTesting 315 static class MagnificationModeInfo extends ItemInfoArrayAdapter.ItemInfo { 316 @MagnificationMode 317 public final int mMagnificationMode; 318 MagnificationModeInfo(@onNull CharSequence title, @Nullable CharSequence summary, @DrawableRes int drawableId, @MagnificationMode int magnificationMode)319 MagnificationModeInfo(@NonNull CharSequence title, @Nullable CharSequence summary, 320 @DrawableRes int drawableId, @MagnificationMode int magnificationMode) { 321 super(title, summary, drawableId); 322 mMagnificationMode = magnificationMode; 323 } 324 } 325 } 326