Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b6bf10eb authored by PETER LIANG's avatar PETER LIANG Committed by Android (Google) Code Review
Browse files

Merge changes from topic "tutorial_improvement" into rvc-dev

* changes:
  Tutorial improvement for Accessibility shortcut (3/n).
  Tutorial improvement for Accessibility shortcut (2/n).
  Tutorial improvement for Accessibility shortcut (1/n).
parents ef2e3193 8cbb44d6
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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">
+26 −0
Original line number Diff line number Diff line
<!--
    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>
+58 −0
Original line number Diff line number Diff line
<!--
    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>
+329 −2
Original line number Diff line number Diff line
@@ -16,35 +16,55 @@

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.annotation.VisibleForTesting;
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 +79,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 +113,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 +230,302 @@ 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);
    }

    @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 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.
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -100,6 +100,12 @@ public class LaunchAccessibilityActivityPreferenceFragment extends
        showDialog(DialogEnums.EDIT_SHORTCUT);
    }

    @Override
    int getUserShortcutTypes() {
        return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(),
                mComponentName);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        // Do not call super. We don't want to see the "Help & feedback" option on this page so as
Loading