/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.accessibility; import static com.android.settings.accessibility.ItemInfoArrayAdapter.ItemInfo; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.icu.text.MessageFormat; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ImageSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RawRes; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import com.android.server.accessibility.Flags; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.utils.AnnotationSpan; import com.android.settingslib.widget.LottieColorUtils; import com.airbnb.lottie.LottieAnimationView; import com.airbnb.lottie.LottieDrawable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** * Utility class for creating the edit dialog. */ public class AccessibilityDialogUtils { private static final String TAG = "AccessibilityDialogUtils"; /** Denotes the dialog emuns for show dialog. */ @Retention(RetentionPolicy.SOURCE) public @interface DialogEnums { /** OPEN: Settings > Accessibility > Any toggle service > Shortcut > Settings. */ int EDIT_SHORTCUT = 1; /** OPEN: Settings > Accessibility > Magnification > Shortcut > Settings. */ int MAGNIFICATION_EDIT_SHORTCUT = 1001; /** * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to * enable service. */ int ENABLE_WARNING_FROM_TOGGLE = 1002; /** OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox. */ int ENABLE_WARNING_FROM_SHORTCUT = 1003; /** * OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox * toggle. */ int ENABLE_WARNING_FROM_SHORTCUT_TOGGLE = 1004; /** * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to * disable service. */ int DISABLE_WARNING_FROM_TOGGLE = 1005; /** * OPEN: Settings > Accessibility > Magnification > Toggle user service in button * navigation. */ int ACCESSIBILITY_BUTTON_TUTORIAL = 1006; /** * OPEN: Settings > Accessibility > Magnification > Toggle user service in gesture * navigation. */ int GESTURE_NAVIGATION_TUTORIAL = 1007; /** * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle user service > Show * launch tutorial. */ int LAUNCH_ACCESSIBILITY_TUTORIAL = 1008; /** * OPEN: Settings > Accessibility > Display size and text > Click 'Reset settings' button. */ int DIALOG_RESET_SETTINGS = 1009; } /** * IntDef enum for dialog type that indicates different dialog for user to choose the shortcut * type. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ DialogType.EDIT_SHORTCUT_GENERIC, DialogType.EDIT_SHORTCUT_GENERIC_SUW, DialogType.EDIT_SHORTCUT_MAGNIFICATION, DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW, }) public @interface DialogType { int EDIT_SHORTCUT_GENERIC = 0; int EDIT_SHORTCUT_GENERIC_SUW = 1; int EDIT_SHORTCUT_MAGNIFICATION = 2; int EDIT_SHORTCUT_MAGNIFICATION_SUW = 3; } /** * Method to show the edit shortcut dialog. * * @param context A valid context * @param dialogType The type of edit shortcut dialog * @param dialogTitle The title of edit shortcut dialog * @param listener The listener to determine the action of edit shortcut dialog * @return A edit shortcut dialog for showing */ public static AlertDialog showEditShortcutDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener) { final AlertDialog alertDialog = createDialog(context, dialogType, dialogTitle, listener); alertDialog.show(); setScrollIndicators(alertDialog); return alertDialog; } /** * Updates the shortcut content in edit shortcut dialog. * * @param context A valid context * @param editShortcutDialog Need to be a type of edit shortcut dialog * @return True if the update is successful */ public static boolean updateShortcutInDialog(Context context, Dialog editShortcutDialog) { final View container = editShortcutDialog.findViewById(R.id.container_layout); if (container != null) { initSoftwareShortcut(context, container); initHardwareShortcut(context, container); return true; } return false; } private static AlertDialog createDialog(Context context, int dialogType, CharSequence dialogTitle, DialogInterface.OnClickListener listener) { final AlertDialog alertDialog = new AlertDialog.Builder(context) .setView(createEditDialogContentView(context, dialogType)) .setTitle(dialogTitle) .setPositiveButton(R.string.save, listener) .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> dialog.dismiss()) .create(); return alertDialog; } /** * Sets the scroll indicators for dialog view. The indicators appears while content view is * out of vision for vertical scrolling. */ private static void setScrollIndicators(AlertDialog dialog) { final ScrollView scrollView = dialog.findViewById(R.id.container_layout); setScrollIndicators(scrollView); } /** * Sets the scroll indicators for dialog view. The indicators appear while content view is * out of vision for vertical scrolling. * * @param view The view contains customized dialog content. Usually it is {@link ScrollView} or * {@link AbsListView} */ private static void setScrollIndicators(@NonNull View view) { view.setScrollIndicators( View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM, View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); } /** * Get a content View for the edit shortcut dialog. * * @param context A valid context * @param dialogType The type of edit shortcut dialog * @return A content view suitable for viewing */ private static View createEditDialogContentView(Context context, int dialogType) { final LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); View contentView = null; switch (dialogType) { case DialogType.EDIT_SHORTCUT_GENERIC: contentView = inflater.inflate( R.layout.accessibility_edit_shortcut, null); initSoftwareShortcut(context, contentView); initHardwareShortcut(context, contentView); break; case DialogType.EDIT_SHORTCUT_GENERIC_SUW: contentView = inflater.inflate( R.layout.accessibility_edit_shortcut, null); initSoftwareShortcutForSUW(context, contentView); initHardwareShortcut(context, contentView); break; case DialogType.EDIT_SHORTCUT_MAGNIFICATION: contentView = inflater.inflate( R.layout.accessibility_edit_shortcut_magnification, null); initSoftwareShortcut(context, contentView); initHardwareShortcut(context, contentView); if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) { initTwoFingerDoubleTapMagnificationShortcut(context, contentView); } initMagnifyShortcut(context, contentView); initAdvancedWidget(contentView); break; case DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW: contentView = inflater.inflate( R.layout.accessibility_edit_shortcut_magnification, null); initSoftwareShortcutForSUW(context, contentView); initHardwareShortcut(context, contentView); if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) { initTwoFingerDoubleTapMagnificationShortcut(context, contentView); } initMagnifyShortcut(context, contentView); initAdvancedWidget(contentView); break; default: throw new IllegalArgumentException(); } return contentView; } private static void setupShortcutWidget(View view, CharSequence titleText, CharSequence summaryText, @DrawableRes int imageResId) { setupShortcutWidgetWithTitleAndSummary(view, titleText, summaryText); setupShortcutWidgetWithImageResource(view, imageResId); } private static void setupShortcutWidgetWithImageRawResource(Context context, View view, CharSequence titleText, CharSequence summaryText, @RawRes int imageRawResId) { setupShortcutWidgetWithTitleAndSummary(view, titleText, summaryText); setupShortcutWidgetWithImageRawResource(context, view, imageRawResId); } private static void setupShortcutWidgetWithTitleAndSummary(View view, CharSequence titleText, CharSequence summaryText) { final CheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(titleText); final TextView summary = view.findViewById(R.id.summary); if (TextUtils.isEmpty(summaryText)) { summary.setVisibility(View.GONE); } else { summary.setText(summaryText); summary.setMovementMethod(LinkMovementMethod.getInstance()); summary.setFocusable(false); } } private static void setupShortcutWidgetWithImageResource(View view, @DrawableRes int imageResId) { final ImageView imageView = view.findViewById(R.id.image); imageView.setImageResource(imageResId); } private static void setupShortcutWidgetWithImageRawResource(Context context, View view, @RawRes int imageRawResId) { final LottieAnimationView lottieView = view.findViewById(R.id.image); lottieView.setFailureListener( result -> Log.w(TAG, "Invalid image raw resource id: " + imageRawResId, result)); lottieView.setAnimation(imageRawResId); lottieView.setRepeatCount(LottieDrawable.INFINITE); LottieColorUtils.applyDynamicColors(context, lottieView); lottieView.playAnimation(); } private static void initSoftwareShortcutForSUW(Context context, View view) { final View dialogView = view.findViewById(R.id.software_shortcut); final CharSequence title = context.getText( R.string.accessibility_shortcut_edit_dialog_title_software); final TextView summary = dialogView.findViewById(R.id.summary); final int lineHeight = summary.getLineHeight(); setupShortcutWidget(dialogView, title, retrieveSoftwareShortcutSummaryForSUW(context, lineHeight), retrieveSoftwareShortcutImageResId(context)); } private static void initSoftwareShortcut(Context context, View view) { final View dialogView = view.findViewById(R.id.software_shortcut); final TextView summary = dialogView.findViewById(R.id.summary); final int lineHeight = summary.getLineHeight(); setupShortcutWidget(dialogView, retrieveTitle(context), retrieveSoftwareShortcutSummary(context, lineHeight), retrieveSoftwareShortcutImageResId(context)); } private static void initHardwareShortcut(Context context, View view) { final View dialogView = view.findViewById(R.id.hardware_shortcut); final CharSequence title = context.getText( R.string.accessibility_shortcut_edit_dialog_title_hardware); final CharSequence summary = context.getText( R.string.accessibility_shortcut_edit_dialog_summary_hardware); setupShortcutWidget(dialogView, title, summary, R.drawable.a11y_shortcut_type_hardware); } private static void initMagnifyShortcut(Context context, View view) { final View dialogView = view.findViewById(R.id.triple_tap_shortcut); final CharSequence title = context.getText( R.string.accessibility_shortcut_edit_dialog_title_triple_tap); String summary = context.getString( R.string.accessibility_shortcut_edit_dialog_summary_triple_tap); // Format the number '3' in the summary. final Object[] arguments = {3}; summary = MessageFormat.format(summary, arguments); setupShortcutWidgetWithImageRawResource(context, dialogView, title, summary, R.raw.a11y_shortcut_type_triple_tap); } private static void initTwoFingerDoubleTapMagnificationShortcut(Context context, View view) { // TODO(b/306153204): Update shortcut string and image when UX provides them final View dialogView = view.findViewById(R.id.two_finger_triple_tap_shortcut); final CharSequence title = context.getText( R.string.accessibility_shortcut_edit_dialog_title_two_finger_double_tap); String summary = context.getString( R.string.accessibility_shortcut_edit_dialog_summary_two_finger_double_tap); // Format the number '2' in the summary. final Object[] arguments = {2}; summary = MessageFormat.format(summary, arguments); setupShortcutWidgetWithImageRawResource(context, dialogView, title, summary, R.raw.a11y_shortcut_type_triple_tap); dialogView.setVisibility(View.VISIBLE); } private static void initAdvancedWidget(View view) { final LinearLayout advanced = view.findViewById(R.id.advanced_shortcut); final View tripleTap = view.findViewById(R.id.triple_tap_shortcut); advanced.setOnClickListener((View v) -> { advanced.setVisibility(View.GONE); tripleTap.setVisibility(View.VISIBLE); }); } private static CharSequence retrieveSoftwareShortcutSummaryForSUW(Context context, int lineHeight) { final SpannableStringBuilder sb = new SpannableStringBuilder(); if (!AccessibilityUtil.isFloatingMenuEnabled(context)) { sb.append(getSummaryStringWithIcon(context, lineHeight)); } return sb; } private static CharSequence retrieveTitle(Context context) { int resId; if (AccessibilityUtil.isFloatingMenuEnabled(context)) { resId = R.string.accessibility_shortcut_edit_dialog_title_software; } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { resId = R.string.accessibility_shortcut_edit_dialog_title_software_by_gesture; } else { resId = R.string.accessibility_shortcut_edit_dialog_title_software; } return context.getText(resId); } private static CharSequence retrieveSoftwareShortcutSummary(Context context, int lineHeight) { final SpannableStringBuilder sb = new SpannableStringBuilder(); if (AccessibilityUtil.isFloatingMenuEnabled(context)) { sb.append(getCustomizeAccessibilityButtonLink(context)); } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { final int resId = AccessibilityUtil.isTouchExploreEnabled(context) ? R.string.accessibility_shortcut_edit_dialog_summary_software_gesture_talkback : R.string.accessibility_shortcut_edit_dialog_summary_software_gesture; sb.append(context.getText(resId)); sb.append("\n\n"); sb.append(getCustomizeAccessibilityButtonLink(context)); } else { sb.append(getSummaryStringWithIcon(context, lineHeight)); sb.append("\n\n"); sb.append(getCustomizeAccessibilityButtonLink(context)); } return sb; } private static int retrieveSoftwareShortcutImageResId(Context context) { int resId; if (AccessibilityUtil.isFloatingMenuEnabled(context)) { resId = R.drawable.a11y_shortcut_type_software_floating; } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { resId = AccessibilityUtil.isTouchExploreEnabled(context) ? R.drawable.a11y_shortcut_type_software_gesture_talkback : R.drawable.a11y_shortcut_type_software_gesture; } else { resId = R.drawable.a11y_shortcut_type_software; } return resId; } private static CharSequence getCustomizeAccessibilityButtonLink(Context context) { final View.OnClickListener linkListener = v -> new SubSettingLauncher(context) .setDestination(AccessibilityButtonFragment.class.getName()) .setSourceMetricsCategory( SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS) .launch(); final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener); return AnnotationSpan.linkify(context.getText( R.string.accessibility_shortcut_edit_dialog_summary_software_floating), linkInfo); } private static SpannableString getSummaryStringWithIcon(Context context, int lineHeight) { final String summary = context .getString(R.string.accessibility_shortcut_edit_dialog_summary_software); final SpannableString spannableMessage = SpannableString.valueOf(summary); // Icon final int indexIconStart = summary.indexOf("%s"); final int indexIconEnd = indexIconStart + 2; final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new); final ImageSpan imageSpan = new ImageSpan(icon); imageSpan.setContentDescription(""); icon.setBounds(0, 0, lineHeight, lineHeight); spannableMessage.setSpan( imageSpan, indexIconStart, indexIconEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannableMessage; } /** * Returns the color associated with the specified attribute in the context's theme. */ @ColorInt private static int getThemeAttrColor(final Context context, final int attributeColor) { final int colorResId = getAttrResourceId(context, attributeColor); return ContextCompat.getColor(context, colorResId); } /** * Returns the identifier of the resolved resource assigned to the given attribute. */ private static int getAttrResourceId(final Context context, final int attributeColor) { final int[] attrs = {attributeColor}; final TypedArray typedArray = context.obtainStyledAttributes(attrs); final int colorResId = typedArray.getResourceId(0, 0); typedArray.recycle(); return colorResId; } /** * Creates a dialog with the given view. * * @param context A valid context * @param dialogTitle The title of the dialog * @param customView The customized view * @param positiveButtonText The text of the positive button * @param positiveListener This listener will be invoked when the positive button in the dialog * is clicked * @param negativeButtonText The text of the negative button * @param negativeListener This listener will be invoked when the negative button in the dialog * is clicked * @return the {@link Dialog} with the given view */ public static Dialog createCustomDialog(Context context, CharSequence dialogTitle, View customView, CharSequence positiveButtonText, DialogInterface.OnClickListener positiveListener, CharSequence negativeButtonText, DialogInterface.OnClickListener negativeListener) { final AlertDialog alertDialog = new AlertDialog.Builder(context) .setView(customView) .setTitle(dialogTitle) .setCancelable(true) .setPositiveButton(positiveButtonText, positiveListener) .setNegativeButton(negativeButtonText, negativeListener) .create(); if (customView instanceof ScrollView || customView instanceof AbsListView) { setScrollIndicators(customView); } return alertDialog; } /** * Creates a single choice {@link ListView} with given {@link ItemInfo} list. * * @param context A context. * @param itemInfoList A {@link ItemInfo} list. * @param itemListener The listener will be invoked when the item is clicked. */ @NonNull public static ListView createSingleChoiceListView(@NonNull Context context, @NonNull List itemInfoList, @Nullable AdapterView.OnItemClickListener itemListener) { final ListView list = new ListView(context); // Set an id to save its state. list.setId(android.R.id.list); list.setDivider(/* divider= */ null); list.setChoiceMode(ListView.CHOICE_MODE_SINGLE); final ItemInfoArrayAdapter adapter = new ItemInfoArrayAdapter(context, itemInfoList); list.setAdapter(adapter); list.setOnItemClickListener(itemListener); return list; } }