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