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