Tutorial improvement for Accessibility shortcut (2/n).

Update UI widgets to meet the new design.

Bug: 148989018
Test: manual test
Change-Id: Ife42995af193db6746135d29f6fa1ad452d265a6
This commit is contained in:
Peter_Liang
2020-03-06 22:54:00 +08:00
parent cfe3e454ac
commit 72aa60ae95
7 changed files with 435 additions and 26 deletions

View File

@@ -15,8 +15,8 @@
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">

View File

@@ -0,0 +1,26 @@
<!--
Copyright (C) 2020 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="#FFFFFF"/>
</vector>

View File

@@ -0,0 +1,58 @@
<!--
Copyright (C) 2020 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.
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textDirection="locale"
android:scrollbarStyle="outsideOverlay">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextSwitcher
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureAllChildren="false" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="176dp"
android:paddingTop="16dp"
android:paddingBottom="16dp" />
<LinearLayout
android:id="@+id/indicator_container"
android:layout_width="wrap_content"
android:layout_height="10dp"
android:layout_gravity="center" />
<TextSwitcher
android:id="@+id/instruction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:measureAllChildren="false" />
</LinearLayout>
</ScrollView>

View File

@@ -16,35 +16,54 @@
package com.android.settings.accessibility;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static com.android.settings.accessibility.AccessibilityUtil.UserShortcutType;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ImageSpan;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextSwitcher;
import android.widget.TextView;
import androidx.annotation.AnimRes;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.util.Preconditions;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.android.settings.R;
import com.android.settings.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for creating the dialog that guides users for gesture navigation for
* accessibility services.
*/
public class AccessibilityGestureNavigationTutorial {
public final class AccessibilityGestureNavigationTutorial {
/** IntDef enum for dialog type. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -59,6 +78,8 @@ public class AccessibilityGestureNavigationTutorial {
int GESTURE_NAVIGATION_SETTINGS = 2;
}
private AccessibilityGestureNavigationTutorial() {}
private static final DialogInterface.OnClickListener mOnClickListener =
(DialogInterface dialog, int which) -> dialog.dismiss();
@@ -91,6 +112,13 @@ public class AccessibilityGestureNavigationTutorial {
return createDialog(context, DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION);
}
static AlertDialog createAccessibilityTutorialDialog(Context context, int shortcutTypes) {
return new AlertDialog.Builder(context)
.setView(createShortcutNavigationContentView(context, shortcutTypes))
.setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener)
.create();
}
/**
* Get a content View for a dialog to confirm that they want to enable a service.
*
@@ -201,4 +229,301 @@ public class AccessibilityGestureNavigationTutorial {
typedArray.recycle();
return colorResId;
}
private static class TutorialPagerAdapter extends PagerAdapter {
private final List<TutorialPage> mTutorialPages;
private TutorialPagerAdapter(List<TutorialPage> tutorialPages) {
this.mTutorialPages = tutorialPages;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
final View itemView = mTutorialPages.get(position).getImageView();
container.addView(itemView);
return itemView;
}
@Override
public int getCount() {
return mTutorialPages.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
return view == o;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position,
@NonNull Object object) {
final View itemView = mTutorialPages.get(position).getImageView();
container.removeView(itemView);
}
}
private static ImageView createImageView(Context context, int imageRes) {
final ImageView imageView = new ImageView(context);
imageView.setImageResource(imageRes);
imageView.setAdjustViewBounds(true);
return imageView;
}
private static View createShortcutNavigationContentView(Context context, int shortcutTypes) {
final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
final View contentView = inflater.inflate(
R.layout.accessibility_shortcut_tutorial_dialog, /* root= */ null);
final List<TutorialPage> tutorialPages =
createShortcutTutorialPages(context, shortcutTypes);
Preconditions.checkArgument(!tutorialPages.isEmpty(),
/* errorMessage= */ "Unexpected tutorial pages size");
final LinearLayout indicatorContainer = contentView.findViewById(R.id.indicator_container);
indicatorContainer.setVisibility(tutorialPages.size() > 1 ? VISIBLE : GONE);
for (TutorialPage page : tutorialPages) {
indicatorContainer.addView(page.getIndicatorIcon());
}
tutorialPages.get(/* firstIndex */ 0).getIndicatorIcon().setEnabled(true);
final TextSwitcher title = contentView.findViewById(R.id.title);
title.setFactory(() -> makeTitleView(context));
title.setText(tutorialPages.get(/* firstIndex */ 0).getTitle());
final TextSwitcher instruction = contentView.findViewById(R.id.instruction);
instruction.setFactory(() -> makeInstructionView(context));
instruction.setText(tutorialPages.get(/* firstIndex */ 0).getInstruction());
final ViewPager viewPager = contentView.findViewById(R.id.view_pager);
viewPager.setAdapter(new TutorialPagerAdapter(tutorialPages));
viewPager.addOnPageChangeListener(
new TutorialPageChangeListener(context, title, instruction, tutorialPages));
return contentView;
}
private static View makeTitleView(Context context) {
final String familyName =
context.getString(
com.android.internal.R.string.config_headlineFontFamilyMedium);
final TextView textView = new TextView(context);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, /* size= */ 20);
textView.setTextColor(Utils.getColorAttr(context, android.R.attr.textColorPrimary));
textView.setGravity(Gravity.CENTER);
textView.setTypeface(Typeface.create(familyName, Typeface.NORMAL));
return textView;
}
private static View makeInstructionView(Context context) {
final TextView textView = new TextView(context);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, /* size= */ 16);
textView.setTextColor(Utils.getColorAttr(context, android.R.attr.textColorPrimary));
textView.setTypeface(
Typeface.create(/* familyName= */ "sans-serif", Typeface.NORMAL));
return textView;
}
private static TutorialPage createSoftwareTutorialPage(@NonNull Context context) {
final CharSequence title = getSoftwareTitle(context);
final ImageView image = createSoftwareImage(context);
final CharSequence instruction = getSoftwareInstruction(context);
final ImageView indicatorIcon =
createImageView(context, R.drawable.ic_accessibility_page_indicator);
indicatorIcon.setEnabled(false);
return new TutorialPage(title, image, indicatorIcon, instruction);
}
private static TutorialPage createHardwareTutorialPage(@NonNull Context context) {
final CharSequence title =
context.getText(R.string.accessibility_tutorial_dialog_title_volume);
final ImageView image =
createImageView(context, R.drawable.accessibility_shortcut_type_hardware);
final ImageView indicatorIcon =
createImageView(context, R.drawable.ic_accessibility_page_indicator);
final CharSequence instruction =
context.getText(R.string.accessibility_tutorial_dialog_message_volume);
indicatorIcon.setEnabled(false);
return new TutorialPage(title, image, indicatorIcon, instruction);
}
private static TutorialPage createTripleTapTutorialPage(@NonNull Context context) {
final CharSequence title =
context.getText(R.string.accessibility_tutorial_dialog_title_triple);
final ImageView image =
createImageView(context, R.drawable.accessibility_shortcut_type_triple_tap);
final CharSequence instruction =
context.getText(R.string.accessibility_tutorial_dialog_message_triple);
final ImageView indicatorIcon =
createImageView(context, R.drawable.ic_accessibility_page_indicator);
indicatorIcon.setEnabled(false);
return new TutorialPage(title, image, indicatorIcon, instruction);
}
private static List<TutorialPage> createShortcutTutorialPages(@NonNull Context context,
int shortcutTypes) {
final List<TutorialPage> tutorialPages = new ArrayList<>();
if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) {
tutorialPages.add(createSoftwareTutorialPage(context));
}
if ((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE) {
tutorialPages.add(createHardwareTutorialPage(context));
}
if ((shortcutTypes & UserShortcutType.TRIPLETAP) == UserShortcutType.TRIPLETAP) {
tutorialPages.add(createTripleTapTutorialPage(context));
}
return tutorialPages;
}
private static CharSequence getSoftwareTitle(Context context) {
final boolean isGestureNavigationEnabled =
AccessibilityUtil.isGestureNavigateEnabled(context);
final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context);
return (isGestureNavigationEnabled || isTouchExploreEnabled)
? context.getText(R.string.accessibility_tutorial_dialog_title_gesture)
: context.getText(R.string.accessibility_tutorial_dialog_title_button);
}
private static ImageView createSoftwareImage(Context context) {
int resId = R.drawable.accessibility_shortcut_type_software;
if (AccessibilityUtil.isGestureNavigateEnabled(context)) {
resId = AccessibilityUtil.isTouchExploreEnabled(context)
? R.drawable.accessibility_shortcut_type_software_gesture_talkback
: R.drawable.accessibility_shortcut_type_software_gesture;
}
return createImageView(context, resId);
}
private static CharSequence getSoftwareInstruction(Context context) {
final boolean isGestureNavigateEnabled =
AccessibilityUtil.isGestureNavigateEnabled(context);
final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context);
int resId = R.string.accessibility_tutorial_dialog_message_button;
if (isGestureNavigateEnabled) {
resId = isTouchExploreEnabled
? R.string.accessibility_tutorial_dialog_message_gesture_talkback
: R.string.accessibility_tutorial_dialog_message_gesture;
}
CharSequence text = context.getText(resId);
if (resId == R.string.accessibility_tutorial_dialog_message_button) {
text = getSoftwareInstructionWithIcon(context, text);
}
return text;
}
private static CharSequence getSoftwareInstructionWithIcon(Context context, CharSequence text) {
final String message = text.toString();
final SpannableString spannableInstruction = SpannableString.valueOf(message);
final int indexIconStart = message.indexOf("%s");
final int indexIconEnd = indexIconStart + 2;
final ImageView iconView = new ImageView(context);
iconView.setImageDrawable(context.getDrawable(R.drawable.ic_accessibility_new));
final Drawable icon = iconView.getDrawable().mutate();
final ImageSpan imageSpan = new ImageSpan(icon);
imageSpan.setContentDescription("");
icon.setBounds(/* left= */ 0, /* top= */ 0,
icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
spannableInstruction.setSpan(imageSpan, indexIconStart, indexIconEnd,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableInstruction;
}
private static class TutorialPage {
private final CharSequence mTitle;
private final ImageView mImageView;
private final ImageView mIndicatorIcon;
private final CharSequence mInstruction;
TutorialPage(CharSequence title, ImageView imageView, ImageView indicatorIcon,
CharSequence instruction) {
this.mTitle = title;
this.mImageView = imageView;
this.mIndicatorIcon = indicatorIcon;
this.mInstruction = instruction;
}
public CharSequence getTitle() {
return mTitle;
}
public ImageView getImageView() {
return mImageView;
}
public ImageView getIndicatorIcon() {
return mIndicatorIcon;
}
public CharSequence getInstruction() {
return mInstruction;
}
}
private static class TutorialPageChangeListener implements ViewPager.OnPageChangeListener {
private int mLastTutorialPagePosition = 0;
private final Context mContext;
private final TextSwitcher mTitle;
private final TextSwitcher mInstruction;
private final List<TutorialPage> mTutorialPages;
TutorialPageChangeListener(Context context, ViewGroup title, ViewGroup instruction,
List<TutorialPage> tutorialPages) {
this.mContext = context;
this.mTitle = (TextSwitcher) title;
this.mInstruction = (TextSwitcher) instruction;
this.mTutorialPages = tutorialPages;
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
// Do nothing.
}
@Override
public void onPageSelected(int position) {
final boolean isPreviousPosition =
mLastTutorialPagePosition > position;
@AnimRes
final int inAnimationResId = isPreviousPosition
? android.R.anim.slide_in_left
: com.android.internal.R.anim.slide_in_right;
@AnimRes
final int outAnimationResId = isPreviousPosition
? android.R.anim.slide_out_right
: com.android.internal.R.anim.slide_out_left;
mTitle.setInAnimation(mContext, inAnimationResId);
mTitle.setOutAnimation(mContext, outAnimationResId);
mTitle.setText(mTutorialPages.get(position).getTitle());
mInstruction.setInAnimation(mContext, inAnimationResId);
mInstruction.setOutAnimation(mContext, outAnimationResId);
mInstruction.setText(mTutorialPages.get(position).getInstruction());
for (TutorialPage page : mTutorialPages) {
page.getIndicatorIcon().setEnabled(false);
}
mTutorialPages.get(position).getIndicatorIcon().setEnabled(true);
mLastTutorialPagePosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
// Do nothing.
}
}
}

View File

@@ -164,16 +164,6 @@ public class ToggleAccessibilityServicePreferenceFragment extends
this::onDialogButtonFromDisableToggleClicked);
break;
}
case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: {
if (AccessibilityUtil.isGestureNavigateEnabled(getPrefContext())) {
mDialog = AccessibilityGestureNavigationTutorial
.showGestureNavigationTutorialDialog(getPrefContext());
} else {
mDialog = AccessibilityGestureNavigationTutorial
.showAccessibilityButtonTutorialDialog(getPrefContext());
}
break;
}
default: {
mDialog = super.onCreateDialog(dialogId);
}
@@ -307,6 +297,7 @@ public class ToggleAccessibilityServicePreferenceFragment extends
} else {
AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
mComponentName);
showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
}
} else {
AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
@@ -420,6 +411,9 @@ public class ToggleAccessibilityServicePreferenceFragment extends
final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, mComponentName);
mIsDialogShown.set(false);
showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
mDialog.dismiss();
mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));

View File

@@ -42,7 +42,6 @@ import android.view.accessibility.AccessibilityManager.TouchExplorationStateChan
import android.widget.CheckBox;
import android.widget.ImageView;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
@@ -250,14 +249,21 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
@Override
public Dialog onCreateDialog(int dialogId) {
Dialog dialog;
switch (dialogId) {
case DialogEnums.EDIT_SHORTCUT:
final CharSequence dialogTitle = getPrefContext().getString(
R.string.accessibility_shortcut_title, mPackageName);
final AlertDialog dialog = AccessibilityEditDialogUtils.showEditShortcutDialog(
dialog = AccessibilityEditDialogUtils.showEditShortcutDialog(
getPrefContext(), dialogTitle, this::callOnAlertDialogCheckboxClicked);
initializeDialogCheckBox(dialog);
return dialog;
case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
dialog = AccessibilityGestureNavigationTutorial
.createAccessibilityTutorialDialog(getPrefContext(),
getUserShortcutTypes());
dialog.setCanceledOnTouchOutside(false);
return dialog;
default:
throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
}
@@ -268,6 +274,8 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
switch (dialogId) {
case DialogEnums.EDIT_SHORTCUT:
return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT;
case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
default:
return SettingsEnums.ACTION_UNKNOWN;
}
@@ -663,6 +671,7 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference
if (preference.isChecked()) {
AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
mComponentName);
showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
} else {
AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
mComponentName);

View File

@@ -224,24 +224,22 @@ public class ToggleScreenMagnificationPreferenceFragment extends
@Override
public Dialog onCreateDialog(int dialogId) {
final AlertDialog dialog;
switch (dialogId) {
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
return AccessibilityGestureNavigationTutorial
.showGestureNavigationTutorialDialog(getPrefContext());
case DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL:
return AccessibilityGestureNavigationTutorial
.showAccessibilityButtonTutorialDialog(getPrefContext());
case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT:
final CharSequence dialogTitle = getPrefContext().getString(
R.string.accessibility_shortcut_title, mPackageName);
final AlertDialog dialog =
AccessibilityEditDialogUtils.showMagnificationEditShortcutDialog(
dialog = AccessibilityEditDialogUtils.showMagnificationEditShortcutDialog(
getPrefContext(), dialogTitle,
this::callOnAlertDialogCheckboxClicked);
initializeDialogCheckBox(dialog);
return dialog;
default:
return super.onCreateDialog(dialogId);
}
throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
}
private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) {
@@ -408,7 +406,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT:
return SettingsEnums.DIALOG_MAGNIFICATION_EDIT_SHORTCUT;
default:
return 0;
return super.getDialogMetricsCategory(dialogId);
}
}
@@ -422,9 +420,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
if (enabled && TextUtils.equals(
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
preferenceKey)) {
showDialog(AccessibilityUtil.isGestureNavigateEnabled(getPrefContext())
? DialogEnums.GESTURE_NAVIGATION_TUTORIAL
: DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL);
showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
}
MagnificationPreferenceFragment.setChecked(getContentResolver(), preferenceKey, enabled);
}
@@ -454,6 +450,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
if (preference.isChecked()) {
optInAllMagnificationValuesToSettings(getPrefContext(), shortcutTypes);
showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
} else {
optOutAllMagnificationValuesFromSettings(getPrefContext(), shortcutTypes);
}