| /* |
| * 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 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.drawable.Drawable; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.style.ImageSpan; |
| 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.annotation.VisibleForTesting; |
| import androidx.appcompat.app.AlertDialog; |
| import androidx.core.content.ContextCompat; |
| import androidx.core.util.Preconditions; |
| import androidx.core.widget.TextViewCompat; |
| import androidx.viewpager.widget.PagerAdapter; |
| import androidx.viewpager.widget.ViewPager; |
| |
| import com.android.settings.R; |
| |
| 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 final class AccessibilityGestureNavigationTutorial { |
| /** IntDef enum for dialog type. */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({ |
| DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, |
| DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION, |
| DialogType.GESTURE_NAVIGATION_SETTINGS, |
| }) |
| |
| private @interface DialogType { |
| int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; |
| int LAUNCH_SERVICE_BY_GESTURE_NAVIGATION = 1; |
| int GESTURE_NAVIGATION_SETTINGS = 2; |
| } |
| |
| private AccessibilityGestureNavigationTutorial() {} |
| |
| private static final DialogInterface.OnClickListener mOnClickListener = |
| (DialogInterface dialog, int which) -> dialog.dismiss(); |
| |
| public static void showGestureNavigationSettingsTutorialDialog(Context context, |
| DialogInterface.OnDismissListener dismissListener) { |
| final AlertDialog alertDialog = new AlertDialog.Builder(context) |
| .setView(createTutorialDialogContentView(context, |
| DialogType.GESTURE_NAVIGATION_SETTINGS)) |
| .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) |
| .setOnDismissListener(dismissListener) |
| .create(); |
| |
| alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |
| alertDialog.setCanceledOnTouchOutside(false); |
| alertDialog.show(); |
| } |
| |
| static AlertDialog showAccessibilityButtonTutorialDialog(Context context) { |
| final AlertDialog alertDialog = createDialog(context, |
| DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON); |
| |
| if (!AccessibilityUtil.isGestureNavigateEnabled(context)) { |
| updateMessageWithIcon(context, alertDialog); |
| } |
| |
| return alertDialog; |
| } |
| |
| static AlertDialog showGestureNavigationTutorialDialog(Context context) { |
| 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. |
| * |
| * @param context A valid context |
| * @param dialogType The type of tutorial dialog |
| * @return A content view suitable for viewing |
| */ |
| private static View createTutorialDialogContentView(Context context, int dialogType) { |
| final LayoutInflater inflater = (LayoutInflater) context.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| |
| View content = null; |
| |
| switch (dialogType) { |
| case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON: |
| content = inflater.inflate( |
| R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); |
| break; |
| case DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION: |
| content = inflater.inflate( |
| R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null); |
| final TextureView gestureTutorialVideo = content.findViewById( |
| R.id.gesture_tutorial_video); |
| final TextView gestureTutorialMessage = content.findViewById( |
| R.id.gesture_tutorial_message); |
| VideoPlayer.create(context, AccessibilityUtil.isTouchExploreEnabled(context) |
| ? R.raw.illustration_accessibility_gesture_three_finger |
| : R.raw.illustration_accessibility_gesture_two_finger, |
| gestureTutorialVideo); |
| gestureTutorialMessage.setText(AccessibilityUtil.isTouchExploreEnabled(context) |
| ? R.string.accessibility_tutorial_dialog_message_gesture_talkback |
| : R.string.accessibility_tutorial_dialog_message_gesture); |
| break; |
| case DialogType.GESTURE_NAVIGATION_SETTINGS: |
| content = inflater.inflate( |
| R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null); |
| final TextureView gestureSettingsTutorialVideo = content.findViewById( |
| R.id.gesture_tutorial_video); |
| final TextView gestureSettingsTutorialMessage = content.findViewById( |
| R.id.gesture_tutorial_message); |
| VideoPlayer.create(context, AccessibilityUtil.isTouchExploreEnabled(context) |
| ? R.raw.illustration_accessibility_gesture_three_finger |
| : R.raw.illustration_accessibility_gesture_two_finger, |
| gestureSettingsTutorialVideo); |
| final int stringResId = AccessibilityUtil.isTouchExploreEnabled(context) |
| ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback |
| : R.string.accessibility_tutorial_dialog_message_gesture_settings; |
| gestureSettingsTutorialMessage.setText(stringResId); |
| break; |
| } |
| |
| return content; |
| } |
| |
| private static AlertDialog createDialog(Context context, int dialogType) { |
| final AlertDialog alertDialog = new AlertDialog.Builder(context) |
| .setView(createTutorialDialogContentView(context, dialogType)) |
| .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) |
| .create(); |
| |
| alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |
| alertDialog.setCanceledOnTouchOutside(false); |
| alertDialog.show(); |
| |
| return alertDialog; |
| } |
| |
| private static void updateMessageWithIcon(Context context, AlertDialog alertDialog) { |
| final TextView gestureTutorialMessage = alertDialog.findViewById( |
| R.id.button_tutorial_message); |
| |
| // Get the textView line height to update [icon] size. Must be called after show() |
| final int lineHeight = gestureTutorialMessage.getLineHeight(); |
| gestureTutorialMessage.setText(getMessageStringWithIcon(context, lineHeight)); |
| } |
| |
| private static SpannableString getMessageStringWithIcon(Context context, int lineHeight) { |
| final String messageString = context |
| .getString(R.string.accessibility_tutorial_dialog_message_button); |
| final SpannableString spannableMessage = SpannableString.valueOf(messageString); |
| |
| // Icon |
| final int indexIconStart = messageString.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; |
| } |
| |
| 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.setContentDescription(context.getString(R.string.accessibility_tutorial_pager, |
| /* firstPage */ 1, tutorialPages.size())); |
| viewPager.setImportantForAccessibility(tutorialPages.size() > 1 |
| ? View.IMPORTANT_FOR_ACCESSIBILITY_YES |
| : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); |
| viewPager.addOnPageChangeListener( |
| new TutorialPageChangeListener(context, viewPager, title, instruction, |
| tutorialPages)); |
| |
| return contentView; |
| } |
| |
| private static View makeTitleView(Context context) { |
| final TextView textView = new TextView(context); |
| // Sets the text color, size, style, hint color, and highlight color from the specified |
| // TextAppearance resource. |
| TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogTitle); |
| textView.setGravity(Gravity.CENTER); |
| return textView; |
| } |
| |
| private static View makeInstructionView(Context context) { |
| final TextView textView = new TextView(context); |
| TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogDescription); |
| return textView; |
| } |
| |
| private static TutorialPage createSoftwareTutorialPage(@NonNull Context context) { |
| final CharSequence title = context.getText( |
| R.string.accessibility_tutorial_dialog_title_button); |
| 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); |
| } |
| |
| @VisibleForTesting |
| 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 ImageView createSoftwareImage(Context context) { |
| final int resId = AccessibilityUtil.isFloatingMenuEnabled(context) |
| ? R.drawable.accessibility_shortcut_type_software_floating |
| : R.drawable.accessibility_shortcut_type_software; |
| |
| return createImageView(context, resId); |
| } |
| |
| private static CharSequence getSoftwareInstruction(Context context) { |
| return AccessibilityUtil.isFloatingMenuEnabled(context) |
| ? context.getText(R.string.accessibility_tutorial_dialog_message_floating_button) |
| : getSoftwareInstructionWithIcon(context, |
| context.getText(R.string.accessibility_tutorial_dialog_message_button)); |
| } |
| |
| 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; |
| private final ViewPager mViewPager; |
| |
| TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title, |
| ViewGroup instruction, List<TutorialPage> tutorialPages) { |
| this.mContext = context; |
| this.mViewPager = viewPager; |
| 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; |
| |
| final int currentPageNumber = position + 1; |
| mViewPager.setContentDescription( |
| mContext.getString(R.string.accessibility_tutorial_pager, |
| currentPageNumber, mTutorialPages.size())); |
| } |
| |
| @Override |
| public void onPageScrollStateChanged(int state) { |
| // Do nothing. |
| } |
| } |
| } |