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.Button; 40 import android.widget.CheckBox; 41 import android.widget.ImageView; 42 import android.widget.LinearLayout; 43 import android.widget.ListView; 44 import android.widget.ScrollView; 45 import android.widget.TextView; 46 47 import androidx.annotation.ColorInt; 48 import androidx.annotation.DrawableRes; 49 import androidx.annotation.IntDef; 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.RawRes; 53 import androidx.appcompat.app.AlertDialog; 54 import androidx.core.content.ContextCompat; 55 56 import com.android.settings.R; 57 import com.android.settings.core.SubSettingLauncher; 58 import com.android.settings.utils.AnnotationSpan; 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 /** 125 * IntDef enum for dialog type that indicates different dialog for user to choose the shortcut 126 * type. 127 */ 128 @Retention(RetentionPolicy.SOURCE) 129 @IntDef({ 130 DialogType.EDIT_SHORTCUT_GENERIC, 131 DialogType.EDIT_SHORTCUT_GENERIC_SUW, 132 DialogType.EDIT_SHORTCUT_MAGNIFICATION, 133 DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW, 134 DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT, 135 }) 136 137 public @interface DialogType { 138 int EDIT_SHORTCUT_GENERIC = 0; 139 int EDIT_SHORTCUT_GENERIC_SUW = 1; 140 int EDIT_SHORTCUT_MAGNIFICATION = 2; 141 int EDIT_SHORTCUT_MAGNIFICATION_SUW = 3; 142 int EDIT_MAGNIFICATION_SWITCH_SHORTCUT = 4; 143 } 144 145 /** 146 * Method to show the edit shortcut dialog. 147 * 148 * @param context A valid context 149 * @param dialogType The type of edit shortcut dialog 150 * @param dialogTitle The title of edit shortcut dialog 151 * @param listener The listener to determine the action of edit shortcut dialog 152 * @return A edit shortcut dialog for showing 153 */ showEditShortcutDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener)154 public static AlertDialog showEditShortcutDialog(Context context, int dialogType, 155 CharSequence dialogTitle, DialogInterface.OnClickListener listener) { 156 final AlertDialog alertDialog = createDialog(context, dialogType, dialogTitle, listener); 157 alertDialog.show(); 158 setScrollIndicators(alertDialog); 159 return alertDialog; 160 } 161 162 /** 163 * Method to show the magnification edit shortcut dialog in Magnification. 164 * 165 * @param context A valid context 166 * @param positiveBtnListener The positive button listener 167 * @return A magnification edit shortcut dialog in Magnification 168 */ createMagnificationSwitchShortcutDialog(Context context, CustomButtonsClickListener positiveBtnListener)169 public static Dialog createMagnificationSwitchShortcutDialog(Context context, 170 CustomButtonsClickListener positiveBtnListener) { 171 final View contentView = createSwitchShortcutDialogContentView(context); 172 final AlertDialog alertDialog = new AlertDialog.Builder(context) 173 .setView(contentView) 174 .setTitle(context.getString( 175 R.string.accessibility_magnification_switch_shortcut_title)) 176 .create(); 177 setCustomButtonsClickListener(alertDialog, contentView, 178 positiveBtnListener, /* negativeBtnListener= */ null); 179 setScrollIndicators(contentView); 180 return alertDialog; 181 } 182 183 /** 184 * Updates the software shortcut in edit shortcut dialog. 185 * 186 * @param context A valid context 187 * @param editShortcutDialog Need to be a type of edit shortcut dialog 188 * @return True if the update is successful 189 */ updateSoftwareShortcutInDialog(Context context, Dialog editShortcutDialog)190 public static boolean updateSoftwareShortcutInDialog(Context context, 191 Dialog editShortcutDialog) { 192 final View container = editShortcutDialog.findViewById(R.id.container_layout); 193 if (container != null) { 194 initSoftwareShortcut(context, container); 195 return true; 196 } 197 return false; 198 } 199 createDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener)200 private static AlertDialog createDialog(Context context, int dialogType, 201 CharSequence dialogTitle, DialogInterface.OnClickListener listener) { 202 203 final AlertDialog alertDialog = new AlertDialog.Builder(context) 204 .setView(createEditDialogContentView(context, dialogType)) 205 .setTitle(dialogTitle) 206 .setPositiveButton(R.string.save, listener) 207 .setNegativeButton(R.string.cancel, 208 (DialogInterface dialog, int which) -> dialog.dismiss()) 209 .create(); 210 211 return alertDialog; 212 } 213 214 /** 215 * Sets the scroll indicators for dialog view. The indicators appears while content view is 216 * out of vision for vertical scrolling. 217 */ setScrollIndicators(AlertDialog dialog)218 private static void setScrollIndicators(AlertDialog dialog) { 219 final ScrollView scrollView = dialog.findViewById(R.id.container_layout); 220 setScrollIndicators(scrollView); 221 } 222 223 /** 224 * Sets the scroll indicators for dialog view. The indicators appear while content view is 225 * out of vision for vertical scrolling. 226 * 227 * @param view The view contains customized dialog content. Usually it is {@link ScrollView} or 228 * {@link AbsListView} 229 */ setScrollIndicators(@onNull View view)230 private static void setScrollIndicators(@NonNull View view) { 231 view.setScrollIndicators( 232 View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM, 233 View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 234 } 235 236 237 interface CustomButtonsClickListener { onClick(@ustomButton int which)238 void onClick(@CustomButton int which); 239 } 240 241 /** 242 * Annotation for customized dialog button type. 243 */ 244 @Retention(RetentionPolicy.SOURCE) 245 @IntDef({ 246 CustomButton.POSITIVE, 247 CustomButton.NEGATIVE, 248 }) 249 250 public @interface CustomButton { 251 int POSITIVE = 1; 252 int NEGATIVE = 2; 253 } 254 setCustomButtonsClickListener(Dialog dialog, View contentView, CustomButtonsClickListener positiveBtnListener, CustomButtonsClickListener negativeBtnListener)255 private static void setCustomButtonsClickListener(Dialog dialog, View contentView, 256 CustomButtonsClickListener positiveBtnListener, 257 CustomButtonsClickListener negativeBtnListener) { 258 final Button positiveButton = contentView.findViewById( 259 R.id.custom_positive_button); 260 final Button negativeButton = contentView.findViewById( 261 R.id.custom_negative_button); 262 263 if (positiveButton != null) { 264 positiveButton.setOnClickListener(v -> { 265 if (positiveBtnListener != null) { 266 positiveBtnListener.onClick(CustomButton.POSITIVE); 267 } 268 dialog.dismiss(); 269 }); 270 } 271 272 if (negativeButton != null) { 273 negativeButton.setOnClickListener(v -> { 274 if (negativeBtnListener != null) { 275 negativeBtnListener.onClick(CustomButton.NEGATIVE); 276 } 277 dialog.dismiss(); 278 }); 279 } 280 } 281 createSwitchShortcutDialogContentView(Context context)282 private static View createSwitchShortcutDialogContentView(Context context) { 283 return createEditDialogContentView(context, DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT); 284 } 285 286 /** 287 * Get a content View for the edit shortcut dialog. 288 * 289 * @param context A valid context 290 * @param dialogType The type of edit shortcut dialog 291 * @return A content view suitable for viewing 292 */ createEditDialogContentView(Context context, int dialogType)293 private static View createEditDialogContentView(Context context, int dialogType) { 294 final LayoutInflater inflater = (LayoutInflater) context.getSystemService( 295 Context.LAYOUT_INFLATER_SERVICE); 296 297 View contentView = null; 298 299 switch (dialogType) { 300 case DialogType.EDIT_SHORTCUT_GENERIC: 301 contentView = inflater.inflate( 302 R.layout.accessibility_edit_shortcut, null); 303 initSoftwareShortcut(context, contentView); 304 initHardwareShortcut(context, contentView); 305 break; 306 case DialogType.EDIT_SHORTCUT_GENERIC_SUW: 307 contentView = inflater.inflate( 308 R.layout.accessibility_edit_shortcut, null); 309 initSoftwareShortcutForSUW(context, contentView); 310 initHardwareShortcut(context, contentView); 311 break; 312 case DialogType.EDIT_SHORTCUT_MAGNIFICATION: 313 contentView = inflater.inflate( 314 R.layout.accessibility_edit_shortcut_magnification, null); 315 initSoftwareShortcut(context, contentView); 316 initHardwareShortcut(context, contentView); 317 initMagnifyShortcut(context, contentView); 318 initAdvancedWidget(contentView); 319 break; 320 case DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW: 321 contentView = inflater.inflate( 322 R.layout.accessibility_edit_shortcut_magnification, null); 323 initSoftwareShortcutForSUW(context, contentView); 324 initHardwareShortcut(context, contentView); 325 initMagnifyShortcut(context, contentView); 326 initAdvancedWidget(contentView); 327 break; 328 case DialogType.EDIT_MAGNIFICATION_SWITCH_SHORTCUT: 329 contentView = inflater.inflate( 330 R.layout.accessibility_edit_magnification_shortcut, null); 331 final ImageView image = contentView.findViewById(R.id.image); 332 image.setImageResource(retrieveSoftwareShortcutImageResId(context)); 333 break; 334 default: 335 throw new IllegalArgumentException(); 336 } 337 338 return contentView; 339 } 340 setupShortcutWidget(View view, CharSequence titleText, CharSequence summaryText, @DrawableRes int imageResId)341 private static void setupShortcutWidget(View view, CharSequence titleText, 342 CharSequence summaryText, @DrawableRes int imageResId) { 343 setupShortcutWidgetWithTitleAndSummary(view, titleText, summaryText); 344 setupShortcutWidgetWithImageResource(view, imageResId); 345 } 346 setupShortcutWidgetWithImageRawResource(View view, CharSequence titleText, CharSequence summaryText, @RawRes int imageRawResId)347 private static void setupShortcutWidgetWithImageRawResource(View view, CharSequence titleText, 348 CharSequence summaryText, @RawRes int imageRawResId) { 349 setupShortcutWidgetWithTitleAndSummary(view, titleText, summaryText); 350 setupShortcutWidgetWithImageRawResource(view, imageRawResId); 351 } 352 setupShortcutWidgetWithTitleAndSummary(View view, CharSequence titleText, CharSequence summaryText)353 private static void setupShortcutWidgetWithTitleAndSummary(View view, CharSequence titleText, 354 CharSequence summaryText) { 355 final CheckBox checkBox = view.findViewById(R.id.checkbox); 356 checkBox.setText(titleText); 357 358 final TextView summary = view.findViewById(R.id.summary); 359 if (TextUtils.isEmpty(summaryText)) { 360 summary.setVisibility(View.GONE); 361 } else { 362 summary.setText(summaryText); 363 summary.setMovementMethod(LinkMovementMethod.getInstance()); 364 summary.setFocusable(false); 365 } 366 } 367 setupShortcutWidgetWithImageResource(View view, @DrawableRes int imageResId)368 private static void setupShortcutWidgetWithImageResource(View view, 369 @DrawableRes int imageResId) { 370 final ImageView imageView = view.findViewById(R.id.image); 371 imageView.setImageResource(imageResId); 372 } 373 setupShortcutWidgetWithImageRawResource(View view, @RawRes int imageRawResId)374 private static void setupShortcutWidgetWithImageRawResource(View view, 375 @RawRes int imageRawResId) { 376 final LottieAnimationView lottieView = view.findViewById(R.id.image); 377 lottieView.setFailureListener( 378 result -> Log.w(TAG, "Invalid image raw resource id: " + imageRawResId, 379 result)); 380 lottieView.setAnimation(imageRawResId); 381 lottieView.setRepeatCount(LottieDrawable.INFINITE); 382 lottieView.playAnimation(); 383 } 384 initSoftwareShortcutForSUW(Context context, View view)385 private static void initSoftwareShortcutForSUW(Context context, View view) { 386 final View dialogView = view.findViewById(R.id.software_shortcut); 387 final CharSequence title = context.getText( 388 R.string.accessibility_shortcut_edit_dialog_title_software); 389 final TextView summary = dialogView.findViewById(R.id.summary); 390 final int lineHeight = summary.getLineHeight(); 391 392 setupShortcutWidget(dialogView, title, 393 retrieveSoftwareShortcutSummaryForSUW(context, lineHeight), 394 retrieveSoftwareShortcutImageResId(context)); 395 } 396 initSoftwareShortcut(Context context, View view)397 private static void initSoftwareShortcut(Context context, View view) { 398 final View dialogView = view.findViewById(R.id.software_shortcut); 399 final TextView summary = dialogView.findViewById(R.id.summary); 400 final int lineHeight = summary.getLineHeight(); 401 402 setupShortcutWidget(dialogView, 403 retrieveTitle(context), 404 retrieveSoftwareShortcutSummary(context, lineHeight), 405 retrieveSoftwareShortcutImageResId(context)); 406 } 407 initHardwareShortcut(Context context, View view)408 private static void initHardwareShortcut(Context context, View view) { 409 final View dialogView = view.findViewById(R.id.hardware_shortcut); 410 final CharSequence title = context.getText( 411 R.string.accessibility_shortcut_edit_dialog_title_hardware); 412 final CharSequence summary = context.getText( 413 R.string.accessibility_shortcut_edit_dialog_summary_hardware); 414 setupShortcutWidget(dialogView, title, summary, 415 R.drawable.accessibility_shortcut_type_hardware); 416 } 417 initMagnifyShortcut(Context context, View view)418 private static void initMagnifyShortcut(Context context, View view) { 419 final View dialogView = view.findViewById(R.id.triple_tap_shortcut); 420 final CharSequence title = context.getText( 421 R.string.accessibility_shortcut_edit_dialog_title_triple_tap); 422 String summary = context.getString( 423 R.string.accessibility_shortcut_edit_dialog_summary_triple_tap); 424 // Format the number '3' in the summary. 425 final Object[] arguments = {3}; 426 summary = MessageFormat.format(summary, arguments); 427 428 setupShortcutWidgetWithImageRawResource(dialogView, title, summary, 429 R.raw.accessibility_shortcut_type_triple_tap); 430 } 431 initAdvancedWidget(View view)432 private static void initAdvancedWidget(View view) { 433 final LinearLayout advanced = view.findViewById(R.id.advanced_shortcut); 434 final View tripleTap = view.findViewById(R.id.triple_tap_shortcut); 435 advanced.setOnClickListener((View v) -> { 436 advanced.setVisibility(View.GONE); 437 tripleTap.setVisibility(View.VISIBLE); 438 }); 439 } 440 retrieveSoftwareShortcutSummaryForSUW(Context context, int lineHeight)441 private static CharSequence retrieveSoftwareShortcutSummaryForSUW(Context context, 442 int lineHeight) { 443 final SpannableStringBuilder sb = new SpannableStringBuilder(); 444 if (!AccessibilityUtil.isFloatingMenuEnabled(context)) { 445 sb.append(getSummaryStringWithIcon(context, lineHeight)); 446 } 447 return sb; 448 } 449 retrieveTitle(Context context)450 private static CharSequence retrieveTitle(Context context) { 451 int resId; 452 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 453 resId = R.string.accessibility_shortcut_edit_dialog_title_software; 454 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 455 resId = R.string.accessibility_shortcut_edit_dialog_title_software_by_gesture; 456 } else { 457 resId = R.string.accessibility_shortcut_edit_dialog_title_software; 458 } 459 return context.getText(resId); 460 } 461 retrieveSoftwareShortcutSummary(Context context, int lineHeight)462 private static CharSequence retrieveSoftwareShortcutSummary(Context context, int lineHeight) { 463 final SpannableStringBuilder sb = new SpannableStringBuilder(); 464 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 465 sb.append(getCustomizeAccessibilityButtonLink(context)); 466 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 467 final int resId = AccessibilityUtil.isTouchExploreEnabled(context) 468 ? R.string.accessibility_shortcut_edit_dialog_summary_software_gesture_talkback 469 : R.string.accessibility_shortcut_edit_dialog_summary_software_gesture; 470 sb.append(context.getText(resId)); 471 sb.append("\n\n"); 472 sb.append(getCustomizeAccessibilityButtonLink(context)); 473 } else { 474 sb.append(getSummaryStringWithIcon(context, lineHeight)); 475 sb.append("\n\n"); 476 sb.append(getCustomizeAccessibilityButtonLink(context)); 477 } 478 return sb; 479 } 480 retrieveSoftwareShortcutImageResId(Context context)481 private static int retrieveSoftwareShortcutImageResId(Context context) { 482 int resId; 483 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 484 resId = R.drawable.accessibility_shortcut_type_software_floating; 485 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 486 resId = AccessibilityUtil.isTouchExploreEnabled(context) 487 ? R.drawable.accessibility_shortcut_type_software_gesture_talkback 488 : R.drawable.accessibility_shortcut_type_software_gesture; 489 } else { 490 resId = R.drawable.accessibility_shortcut_type_software; 491 } 492 return resId; 493 } 494 getCustomizeAccessibilityButtonLink(Context context)495 private static CharSequence getCustomizeAccessibilityButtonLink(Context context) { 496 final View.OnClickListener linkListener = v -> new SubSettingLauncher(context) 497 .setDestination(AccessibilityButtonFragment.class.getName()) 498 .setSourceMetricsCategory( 499 SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS) 500 .launch(); 501 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( 502 AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener); 503 return AnnotationSpan.linkify(context.getText( 504 R.string.accessibility_shortcut_edit_dialog_summary_software_floating), linkInfo); 505 } 506 getSummaryStringWithIcon(Context context, int lineHeight)507 private static SpannableString getSummaryStringWithIcon(Context context, int lineHeight) { 508 final String summary = context 509 .getString(R.string.accessibility_shortcut_edit_dialog_summary_software); 510 final SpannableString spannableMessage = SpannableString.valueOf(summary); 511 512 // Icon 513 final int indexIconStart = summary.indexOf("%s"); 514 final int indexIconEnd = indexIconStart + 2; 515 final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new); 516 final ImageSpan imageSpan = new ImageSpan(icon); 517 imageSpan.setContentDescription(""); 518 icon.setBounds(0, 0, lineHeight, lineHeight); 519 spannableMessage.setSpan( 520 imageSpan, indexIconStart, indexIconEnd, 521 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 522 return spannableMessage; 523 } 524 525 /** 526 * Returns the color associated with the specified attribute in the context's theme. 527 */ 528 @ColorInt getThemeAttrColor(final Context context, final int attributeColor)529 private static int getThemeAttrColor(final Context context, final int attributeColor) { 530 final int colorResId = getAttrResourceId(context, attributeColor); 531 return ContextCompat.getColor(context, colorResId); 532 } 533 534 /** 535 * Returns the identifier of the resolved resource assigned to the given attribute. 536 */ getAttrResourceId(final Context context, final int attributeColor)537 private static int getAttrResourceId(final Context context, final int attributeColor) { 538 final int[] attrs = {attributeColor}; 539 final TypedArray typedArray = context.obtainStyledAttributes(attrs); 540 final int colorResId = typedArray.getResourceId(0, 0); 541 typedArray.recycle(); 542 return colorResId; 543 } 544 545 /** 546 * Creates a dialog with the given view. 547 * 548 * @param context A valid context 549 * @param dialogTitle The title of the dialog 550 * @param customView The customized view 551 * @param listener This listener will be invoked when the positive button in the dialog is 552 * clicked 553 * @return the {@link Dialog} with the given view 554 */ createCustomDialog(Context context, CharSequence dialogTitle, View customView, DialogInterface.OnClickListener listener)555 public static Dialog createCustomDialog(Context context, CharSequence dialogTitle, 556 View customView, DialogInterface.OnClickListener listener) { 557 final AlertDialog alertDialog = new AlertDialog.Builder(context) 558 .setView(customView) 559 .setTitle(dialogTitle) 560 .setCancelable(true) 561 .setPositiveButton(R.string.save, listener) 562 .setNegativeButton(R.string.cancel, null) 563 .create(); 564 if (customView instanceof ScrollView || customView instanceof AbsListView) { 565 setScrollIndicators(customView); 566 } 567 return alertDialog; 568 } 569 570 /** 571 * Creates a single choice {@link ListView} with given {@link ItemInfo} list. 572 * 573 * @param context A context. 574 * @param itemInfoList A {@link ItemInfo} list. 575 * @param itemListener The listener will be invoked when the item is clicked. 576 */ 577 @NonNull createSingleChoiceListView(@onNull Context context, @NonNull List<? extends ItemInfo> itemInfoList, @Nullable AdapterView.OnItemClickListener itemListener)578 public static ListView createSingleChoiceListView(@NonNull Context context, 579 @NonNull List<? extends ItemInfo> itemInfoList, 580 @Nullable AdapterView.OnItemClickListener itemListener) { 581 final ListView list = new ListView(context); 582 // Set an id to save its state. 583 list.setId(android.R.id.list); 584 list.setDivider(/* divider= */ null); 585 list.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 586 final ItemInfoArrayAdapter 587 adapter = new ItemInfoArrayAdapter(context, itemInfoList); 588 list.setAdapter(adapter); 589 list.setOnItemClickListener(itemListener); 590 return list; 591 } 592 } 593