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