blob: c429e0b0d559e241230e8955c5a5b60df023d57b [file] [log] [blame]
/*
* 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()) {
initTwoFingerTripleTapMagnificationShortcut(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()) {
initTwoFingerTripleTapMagnificationShortcut(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 initTwoFingerTripleTapMagnificationShortcut(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_triple_tap);
String summary = context.getString(
R.string.accessibility_shortcut_edit_dialog_summary_two_finger_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);
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<? extends ItemInfo> 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;
}
}