• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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