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