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.ItemInfoArrayAdapter.ItemInfo; 20 21 import android.app.Dialog; 22 import android.app.settings.SettingsEnums; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.res.TypedArray; 26 import android.graphics.drawable.Drawable; 27 import android.icu.text.MessageFormat; 28 import android.text.Spannable; 29 import android.text.SpannableString; 30 import android.text.SpannableStringBuilder; 31 import android.text.TextUtils; 32 import android.text.method.LinkMovementMethod; 33 import android.text.style.ImageSpan; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.widget.AbsListView; 37 import android.widget.AdapterView; 38 import android.widget.Button; 39 import android.widget.CheckBox; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 import android.widget.ListView; 43 import android.widget.ScrollView; 44 import android.widget.TextView; 45 46 import androidx.annotation.ColorInt; 47 import androidx.annotation.IntDef; 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 import androidx.appcompat.app.AlertDialog; 51 import androidx.core.content.ContextCompat; 52 53 import com.android.settings.R; 54 import com.android.settings.core.SubSettingLauncher; 55 import com.android.settings.utils.AnnotationSpan; 56 57 import java.lang.annotation.Retention; 58 import java.lang.annotation.RetentionPolicy; 59 import java.util.List; 60 61 62 /** 63 * Utility class for creating the edit dialog. 64 */ 65 public class AccessibilityDialogUtils { 66 67 /** Denotes the dialog emuns for show dialog. */ 68 @Retention(RetentionPolicy.SOURCE) 69 public @interface DialogEnums { 70 71 /** OPEN: Settings > Accessibility > Any toggle service > Shortcut > Settings. */ 72 int EDIT_SHORTCUT = 1; 73 74 /** OPEN: Settings > Accessibility > Magnification > Shortcut > Settings. */ 75 int MAGNIFICATION_EDIT_SHORTCUT = 1001; 76 77 /** 78 * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to 79 * enable service. 80 */ 81 int ENABLE_WARNING_FROM_TOGGLE = 1002; 82 83 /** OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox. */ 84 int ENABLE_WARNING_FROM_SHORTCUT = 1003; 85 86 /** 87 * OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox 88 * toggle. 89 */ 90 int ENABLE_WARNING_FROM_SHORTCUT_TOGGLE = 1004; 91 92 /** 93 * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to 94 * disable service. 95 */ 96 int DISABLE_WARNING_FROM_TOGGLE = 1005; 97 98 /** 99 * OPEN: Settings > Accessibility > Magnification > Toggle user service in button 100 * navigation. 101 */ 102 int ACCESSIBILITY_BUTTON_TUTORIAL = 1006; 103 104 /** 105 * OPEN: Settings > Accessibility > Magnification > Toggle user service in gesture 106 * navigation. 107 */ 108 int GESTURE_NAVIGATION_TUTORIAL = 1007; 109 110 /** 111 * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle user service > Show 112 * launch tutorial. 113 */ 114 int LAUNCH_ACCESSIBILITY_TUTORIAL = 1008; 115 } 116 117 /** 118 * IntDef enum for dialog type that indicates different dialog for user to choose the shortcut 119 * type. 120 */ 121 @Retention(RetentionPolicy.SOURCE) 122 @IntDef({ 123 DialogType.EDIT_SHORTCUT_GENERIC, 124 DialogType.EDIT_SHORTCUT_GENERIC_SUW, 125 DialogType.EDIT_SHORTCUT_MAGNIFICATION, 126 DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW, 127 DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT, 128 }) 129 130 public @interface DialogType { 131 int EDIT_SHORTCUT_GENERIC = 0; 132 int EDIT_SHORTCUT_GENERIC_SUW = 1; 133 int EDIT_SHORTCUT_MAGNIFICATION = 2; 134 int EDIT_SHORTCUT_MAGNIFICATION_SUW = 3; 135 int EDIT_MAGNIFICATION_SWITCH_SHORTCUT = 4; 136 } 137 138 /** 139 * Method to show the edit shortcut dialog. 140 * 141 * @param context A valid context 142 * @param dialogType The type of edit shortcut dialog 143 * @param dialogTitle The title of edit shortcut dialog 144 * @param listener The listener to determine the action of edit shortcut dialog 145 * @return A edit shortcut dialog for showing 146 */ showEditShortcutDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener)147 public static AlertDialog showEditShortcutDialog(Context context, int dialogType, 148 CharSequence dialogTitle, DialogInterface.OnClickListener listener) { 149 final AlertDialog alertDialog = createDialog(context, dialogType, dialogTitle, listener); 150 alertDialog.show(); 151 setScrollIndicators(alertDialog); 152 return alertDialog; 153 } 154 155 /** 156 * Method to show the magnification edit shortcut dialog in Magnification. 157 * 158 * @param context A valid context 159 * @param positiveBtnListener The positive button listener 160 * @return A magnification edit shortcut dialog in Magnification 161 */ createMagnificationSwitchShortcutDialog(Context context, CustomButtonsClickListener positiveBtnListener)162 public static Dialog createMagnificationSwitchShortcutDialog(Context context, 163 CustomButtonsClickListener positiveBtnListener) { 164 final View contentView = createSwitchShortcutDialogContentView(context); 165 final AlertDialog alertDialog = new AlertDialog.Builder(context) 166 .setView(contentView) 167 .setTitle(context.getString( 168 R.string.accessibility_magnification_switch_shortcut_title)) 169 .create(); 170 setCustomButtonsClickListener(alertDialog, contentView, 171 positiveBtnListener, /* negativeBtnListener= */ null); 172 setScrollIndicators(contentView); 173 return alertDialog; 174 } 175 createDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener)176 private static AlertDialog createDialog(Context context, int dialogType, 177 CharSequence dialogTitle, DialogInterface.OnClickListener listener) { 178 179 final AlertDialog alertDialog = new AlertDialog.Builder(context) 180 .setView(createEditDialogContentView(context, dialogType)) 181 .setTitle(dialogTitle) 182 .setPositiveButton(R.string.save, listener) 183 .setNegativeButton(R.string.cancel, 184 (DialogInterface dialog, int which) -> dialog.dismiss()) 185 .create(); 186 187 return alertDialog; 188 } 189 190 /** 191 * Sets the scroll indicators for dialog view. The indicators appears while content view is 192 * out of vision for vertical scrolling. 193 */ setScrollIndicators(AlertDialog dialog)194 private static void setScrollIndicators(AlertDialog dialog) { 195 final ScrollView scrollView = dialog.findViewById(R.id.container_layout); 196 setScrollIndicators(scrollView); 197 } 198 199 /** 200 * Sets the scroll indicators for dialog view. The indicators appear while content view is 201 * out of vision for vertical scrolling. 202 * 203 * @param view The view contains customized dialog content. Usually it is {@link ScrollView} or 204 * {@link AbsListView} 205 */ setScrollIndicators(@onNull View view)206 private static void setScrollIndicators(@NonNull View view) { 207 view.setScrollIndicators( 208 View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM, 209 View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 210 } 211 212 213 interface CustomButtonsClickListener { onClick(@ustomButton int which)214 void onClick(@CustomButton int which); 215 } 216 217 /** 218 * Annotation for customized dialog button type. 219 */ 220 @Retention(RetentionPolicy.SOURCE) 221 @IntDef({ 222 CustomButton.POSITIVE, 223 CustomButton.NEGATIVE, 224 }) 225 226 public @interface CustomButton { 227 int POSITIVE = 1; 228 int NEGATIVE = 2; 229 } 230 setCustomButtonsClickListener(Dialog dialog, View contentView, CustomButtonsClickListener positiveBtnListener, CustomButtonsClickListener negativeBtnListener)231 private static void setCustomButtonsClickListener(Dialog dialog, View contentView, 232 CustomButtonsClickListener positiveBtnListener, 233 CustomButtonsClickListener negativeBtnListener) { 234 final Button positiveButton = contentView.findViewById( 235 R.id.custom_positive_button); 236 final Button negativeButton = contentView.findViewById( 237 R.id.custom_negative_button); 238 239 if (positiveButton != null) { 240 positiveButton.setOnClickListener(v -> { 241 if (positiveBtnListener != null) { 242 positiveBtnListener.onClick(CustomButton.POSITIVE); 243 } 244 dialog.dismiss(); 245 }); 246 } 247 248 if (negativeButton != null) { 249 negativeButton.setOnClickListener(v -> { 250 if (negativeBtnListener != null) { 251 negativeBtnListener.onClick(CustomButton.NEGATIVE); 252 } 253 dialog.dismiss(); 254 }); 255 } 256 } 257 createSwitchShortcutDialogContentView(Context context)258 private static View createSwitchShortcutDialogContentView(Context context) { 259 return createEditDialogContentView(context, DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT); 260 } 261 262 /** 263 * Get a content View for the edit shortcut dialog. 264 * 265 * @param context A valid context 266 * @param dialogType The type of edit shortcut dialog 267 * @return A content view suitable for viewing 268 */ createEditDialogContentView(Context context, int dialogType)269 private static View createEditDialogContentView(Context context, int dialogType) { 270 final LayoutInflater inflater = (LayoutInflater) context.getSystemService( 271 Context.LAYOUT_INFLATER_SERVICE); 272 273 View contentView = null; 274 275 switch (dialogType) { 276 case DialogType.EDIT_SHORTCUT_GENERIC: 277 contentView = inflater.inflate( 278 R.layout.accessibility_edit_shortcut, null); 279 initSoftwareShortcut(context, contentView); 280 initHardwareShortcut(context, contentView); 281 break; 282 case DialogType.EDIT_SHORTCUT_GENERIC_SUW: 283 contentView = inflater.inflate( 284 R.layout.accessibility_edit_shortcut, null); 285 initSoftwareShortcutForSUW(context, contentView); 286 initHardwareShortcut(context, contentView); 287 break; 288 case DialogType.EDIT_SHORTCUT_MAGNIFICATION: 289 contentView = inflater.inflate( 290 R.layout.accessibility_edit_shortcut_magnification, null); 291 initSoftwareShortcut(context, contentView); 292 initHardwareShortcut(context, contentView); 293 initMagnifyShortcut(context, contentView); 294 initAdvancedWidget(contentView); 295 break; 296 case DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW: 297 contentView = inflater.inflate( 298 R.layout.accessibility_edit_shortcut_magnification, null); 299 initSoftwareShortcutForSUW(context, contentView); 300 initHardwareShortcut(context, contentView); 301 initMagnifyShortcut(context, contentView); 302 initAdvancedWidget(contentView); 303 break; 304 case DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT: 305 contentView = inflater.inflate( 306 R.layout.accessibility_edit_magnification_shortcut, null); 307 final ImageView image = contentView.findViewById(R.id.image); 308 image.setImageResource(retrieveSoftwareShortcutImageResId(context)); 309 break; 310 default: 311 throw new IllegalArgumentException(); 312 } 313 314 return contentView; 315 } 316 setupShortcutWidget(View view, CharSequence titleText, CharSequence summaryText, int imageResId)317 private static void setupShortcutWidget(View view, CharSequence titleText, 318 CharSequence summaryText, int imageResId) { 319 final CheckBox checkBox = view.findViewById(R.id.checkbox); 320 checkBox.setText(titleText); 321 final TextView summary = view.findViewById(R.id.summary); 322 if (TextUtils.isEmpty(summaryText)) { 323 summary.setVisibility(View.GONE); 324 } else { 325 summary.setText(summaryText); 326 summary.setMovementMethod(LinkMovementMethod.getInstance()); 327 summary.setFocusable(false); 328 } 329 final ImageView image = view.findViewById(R.id.image); 330 image.setImageResource(imageResId); 331 } 332 initSoftwareShortcutForSUW(Context context, View view)333 private static void initSoftwareShortcutForSUW(Context context, View view) { 334 final View dialogView = view.findViewById(R.id.software_shortcut); 335 final CharSequence title = context.getText( 336 R.string.accessibility_shortcut_edit_dialog_title_software); 337 final TextView summary = dialogView.findViewById(R.id.summary); 338 final int lineHeight = summary.getLineHeight(); 339 340 setupShortcutWidget(dialogView, title, 341 retrieveSoftwareShortcutSummaryForSUW(context, lineHeight), 342 retrieveSoftwareShortcutImageResId(context)); 343 } 344 initSoftwareShortcut(Context context, View view)345 private static void initSoftwareShortcut(Context context, View view) { 346 final View dialogView = view.findViewById(R.id.software_shortcut); 347 final CharSequence title = context.getText( 348 R.string.accessibility_shortcut_edit_dialog_title_software); 349 final TextView summary = dialogView.findViewById(R.id.summary); 350 final int lineHeight = summary.getLineHeight(); 351 352 setupShortcutWidget(dialogView, title, 353 retrieveSoftwareShortcutSummary(context, lineHeight), 354 retrieveSoftwareShortcutImageResId(context)); 355 } 356 initHardwareShortcut(Context context, View view)357 private static void initHardwareShortcut(Context context, View view) { 358 final View dialogView = view.findViewById(R.id.hardware_shortcut); 359 final CharSequence title = context.getText( 360 R.string.accessibility_shortcut_edit_dialog_title_hardware); 361 final CharSequence summary = context.getText( 362 R.string.accessibility_shortcut_edit_dialog_summary_hardware); 363 setupShortcutWidget(dialogView, title, summary, 364 R.drawable.accessibility_shortcut_type_hardware); 365 // TODO(b/142531156): Use vector drawable instead of temporal png file to avoid distorted. 366 } 367 initMagnifyShortcut(Context context, View view)368 private static void initMagnifyShortcut(Context context, View view) { 369 final View dialogView = view.findViewById(R.id.triple_tap_shortcut); 370 final CharSequence title = context.getText( 371 R.string.accessibility_shortcut_edit_dialog_title_triple_tap); 372 String summary = context.getString( 373 R.string.accessibility_shortcut_edit_dialog_summary_triple_tap); 374 // Format the number '3' in the summary. 375 final Object[] arguments = {3}; 376 summary = MessageFormat.format(summary, arguments); 377 378 setupShortcutWidget(dialogView, title, summary, 379 R.drawable.accessibility_shortcut_type_triple_tap); 380 // TODO(b/142531156): Use vector drawable instead of temporal png file to avoid distorted. 381 } 382 initAdvancedWidget(View view)383 private static void initAdvancedWidget(View view) { 384 final LinearLayout advanced = view.findViewById(R.id.advanced_shortcut); 385 final View tripleTap = view.findViewById(R.id.triple_tap_shortcut); 386 advanced.setOnClickListener((View v) -> { 387 advanced.setVisibility(View.GONE); 388 tripleTap.setVisibility(View.VISIBLE); 389 }); 390 } 391 retrieveSoftwareShortcutSummaryForSUW(Context context, int lineHeight)392 private static CharSequence retrieveSoftwareShortcutSummaryForSUW(Context context, 393 int lineHeight) { 394 final SpannableStringBuilder sb = new SpannableStringBuilder(); 395 if (!AccessibilityUtil.isFloatingMenuEnabled(context)) { 396 sb.append(getSummaryStringWithIcon(context, lineHeight)); 397 } 398 return sb; 399 } 400 retrieveSoftwareShortcutSummary(Context context, int lineHeight)401 private static CharSequence retrieveSoftwareShortcutSummary(Context context, int lineHeight) { 402 final SpannableStringBuilder sb = new SpannableStringBuilder(); 403 if (!AccessibilityUtil.isFloatingMenuEnabled(context)) { 404 sb.append(getSummaryStringWithIcon(context, lineHeight)); 405 sb.append("\n\n"); 406 } 407 sb.append(getCustomizeAccessibilityButtonLink(context)); 408 return sb; 409 } 410 retrieveSoftwareShortcutImageResId(Context context)411 private static int retrieveSoftwareShortcutImageResId(Context context) { 412 return AccessibilityUtil.isFloatingMenuEnabled(context) 413 ? R.drawable.accessibility_shortcut_type_software_floating 414 : R.drawable.accessibility_shortcut_type_software; 415 } 416 getCustomizeAccessibilityButtonLink(Context context)417 private static CharSequence getCustomizeAccessibilityButtonLink(Context context) { 418 final View.OnClickListener linkListener = v -> new SubSettingLauncher(context) 419 .setDestination(AccessibilityButtonFragment.class.getName()) 420 .setSourceMetricsCategory( 421 SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS) 422 .launch(); 423 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( 424 AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener); 425 426 return AnnotationSpan.linkify(context.getText( 427 R.string.accessibility_shortcut_edit_dialog_summary_software_floating), linkInfo); 428 } 429 getSummaryStringWithIcon(Context context, int lineHeight)430 private static SpannableString getSummaryStringWithIcon(Context context, int lineHeight) { 431 final String summary = context 432 .getString(R.string.accessibility_shortcut_edit_dialog_summary_software); 433 final SpannableString spannableMessage = SpannableString.valueOf(summary); 434 435 // Icon 436 final int indexIconStart = summary.indexOf("%s"); 437 final int indexIconEnd = indexIconStart + 2; 438 final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new); 439 final ImageSpan imageSpan = new ImageSpan(icon); 440 imageSpan.setContentDescription(""); 441 icon.setBounds(0, 0, lineHeight, lineHeight); 442 spannableMessage.setSpan( 443 imageSpan, indexIconStart, indexIconEnd, 444 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 445 return spannableMessage; 446 } 447 448 /** 449 * Returns the color associated with the specified attribute in the context's theme. 450 */ 451 @ColorInt getThemeAttrColor(final Context context, final int attributeColor)452 private static int getThemeAttrColor(final Context context, final int attributeColor) { 453 final int colorResId = getAttrResourceId(context, attributeColor); 454 return ContextCompat.getColor(context, colorResId); 455 } 456 457 /** 458 * Returns the identifier of the resolved resource assigned to the given attribute. 459 */ getAttrResourceId(final Context context, final int attributeColor)460 private static int getAttrResourceId(final Context context, final int attributeColor) { 461 final int[] attrs = {attributeColor}; 462 final TypedArray typedArray = context.obtainStyledAttributes(attrs); 463 final int colorResId = typedArray.getResourceId(0, 0); 464 typedArray.recycle(); 465 return colorResId; 466 } 467 468 /** 469 * Creates a dialog with the given view. 470 * 471 * @param context A valid context 472 * @param dialogTitle The title of the dialog 473 * @param customView The customized view 474 * @param listener This listener will be invoked when the positive button in the dialog is 475 * clicked 476 * @return the {@link Dialog} with the given view 477 */ createCustomDialog(Context context, CharSequence dialogTitle, View customView, DialogInterface.OnClickListener listener)478 public static Dialog createCustomDialog(Context context, CharSequence dialogTitle, 479 View customView, DialogInterface.OnClickListener listener) { 480 final AlertDialog alertDialog = new AlertDialog.Builder(context) 481 .setView(customView) 482 .setTitle(dialogTitle) 483 .setCancelable(true) 484 .setPositiveButton(R.string.save, listener) 485 .setNegativeButton(R.string.cancel, null) 486 .create(); 487 if (customView instanceof ScrollView || customView instanceof AbsListView) { 488 setScrollIndicators(customView); 489 } 490 return alertDialog; 491 } 492 493 /** 494 * Creates a single choice {@link ListView} with given {@link ItemInfo} list. 495 * 496 * @param context A context. 497 * @param itemInfoList A {@link ItemInfo} list. 498 * @param itemListener The listener will be invoked when the item is clicked. 499 */ 500 @NonNull createSingleChoiceListView(@onNull Context context, @NonNull List<? extends ItemInfo> itemInfoList, @Nullable AdapterView.OnItemClickListener itemListener)501 public static ListView createSingleChoiceListView(@NonNull Context context, 502 @NonNull List<? extends ItemInfo> itemInfoList, 503 @Nullable AdapterView.OnItemClickListener itemListener) { 504 final ListView list = new ListView(context); 505 // Set an id to save its state. 506 list.setId(android.R.id.list); 507 list.setDivider(/* divider= */ null); 508 list.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 509 final ItemInfoArrayAdapter 510 adapter = new ItemInfoArrayAdapter(context, itemInfoList); 511 list.setAdapter(adapter); 512 list.setOnItemClickListener(itemListener); 513 return list; 514 } 515 } 516