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

Commit cb655881 authored by Peter Liang's avatar Peter Liang
Browse files

Refactor the design and improve the animations of Accessibility Floating Menu(10/n).

Actions:
1) Add the migration tooltip into to indicate users where they could switch the floating menu mode.

Bug: 227715451
Test: atest MenuEduTooltipViewTest MenuListViewTouchHandlerTest
Change-Id: I53ea56b86c38bfcf71431c03c7af6f1d4900a232
parent 615213dd
Loading
Loading
Loading
Loading
+222 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.accessibility.floatingmenu;

import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.UNSPECIFIED;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape;

/**
 * The tooltip view shows the information about the operation of the anchor view {@link MenuView}
 * . It's just shown on the left or right of the anchor view.
 */
@SuppressLint("ViewConstructor")
class MenuEduTooltipView extends FrameLayout {
    private int mFontSize;
    private int mTextViewMargin;
    private int mTextViewPadding;
    private int mTextViewCornerRadius;
    private int mArrowMargin;
    private int mArrowWidth;
    private int mArrowHeight;
    private int mArrowCornerRadius;
    private int mColorAccentPrimary;
    private View mArrowLeftView;
    private View mArrowRightView;
    private TextView mMessageView;
    private final MenuViewAppearance mMenuViewAppearance;

    MenuEduTooltipView(@NonNull Context context, MenuViewAppearance menuViewAppearance) {
        super(context);

        mMenuViewAppearance = menuViewAppearance;

        updateResources();
        initViews();
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        updateResources();
        updateMessageView();
        updateArrowView();

        updateLocationAndVisibility();
    }

    void show(CharSequence message) {
        mMessageView.setText(message);

        updateLocationAndVisibility();
    }

    void updateLocationAndVisibility() {
        final boolean isTooltipOnRightOfAnchor = mMenuViewAppearance.isMenuOnLeftSide();
        updateArrowVisibilityWith(isTooltipOnRightOfAnchor);
        updateLocationWith(getMenuBoundsInParent(), isTooltipOnRightOfAnchor);
    }

    /**
     * Gets the bounds of the {@link MenuView}. Besides, its parent view {@link MenuViewLayer} is
     * also the root view of the tooltip view.
     *
     * @return The menu bounds based on its parent view.
     */
    private Rect getMenuBoundsInParent() {
        final Rect bounds = new Rect();
        final PointF position = mMenuViewAppearance.getMenuPosition();

        bounds.set((int) position.x, (int) position.y,
                (int) position.x + mMenuViewAppearance.getMenuWidth(),
                (int) position.y + mMenuViewAppearance.getMenuHeight());

        return bounds;
    }

    private void updateResources() {
        final Resources res = getResources();

        mArrowWidth =
                res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_width);
        mArrowHeight =
                res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_arrow_height);
        mArrowMargin =
                res.getDimensionPixelSize(
                        R.dimen.accessibility_floating_tooltip_arrow_margin);
        mArrowCornerRadius =
                res.getDimensionPixelSize(
                        R.dimen.accessibility_floating_tooltip_arrow_corner_radius);
        mFontSize =
                res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_font_size);
        mTextViewMargin =
                res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_margin);
        mTextViewPadding =
                res.getDimensionPixelSize(R.dimen.accessibility_floating_tooltip_padding);
        mTextViewCornerRadius =
                res.getDimensionPixelSize(
                        R.dimen.accessibility_floating_tooltip_text_corner_radius);
        mColorAccentPrimary = Utils.getColorAttrDefaultColor(getContext(),
                com.android.internal.R.attr.colorAccentPrimary);
    }

    private void updateLocationWith(Rect anchorBoundsInParent, boolean isTooltipOnRightOfAnchor) {
        final int widthSpec = MeasureSpec.makeMeasureSpec(
                getAvailableTextViewWidth(isTooltipOnRightOfAnchor), AT_MOST);
        final int heightSpec = MeasureSpec.makeMeasureSpec(/* size= */ 0, UNSPECIFIED);
        mMessageView.measure(widthSpec, heightSpec);
        final LinearLayout.LayoutParams textViewParams =
                (LinearLayout.LayoutParams) mMessageView.getLayoutParams();
        textViewParams.width = mMessageView.getMeasuredWidth();
        mMessageView.setLayoutParams(textViewParams);

        final int layoutWidth = mMessageView.getMeasuredWidth() + mArrowWidth + mArrowMargin;
        setTranslationX(isTooltipOnRightOfAnchor
                ? anchorBoundsInParent.right
                : anchorBoundsInParent.left - layoutWidth);

        setTranslationY(anchorBoundsInParent.centerY() - (mMessageView.getMeasuredHeight() / 2.0f));
    }

    private void updateMessageView() {
        mMessageView.setTextSize(COMPLEX_UNIT_PX, mFontSize);
        mMessageView.setPadding(mTextViewPadding, mTextViewPadding, mTextViewPadding,
                mTextViewPadding);

        final GradientDrawable gradientDrawable = (GradientDrawable) mMessageView.getBackground();
        gradientDrawable.setCornerRadius(mTextViewCornerRadius);
        gradientDrawable.setColor(mColorAccentPrimary);
    }

    private void updateArrowView() {
        drawArrow(mArrowLeftView, /* isPointingLeft= */ true);
        drawArrow(mArrowRightView, /* isPointingLeft= */ false);
    }

    private void updateArrowVisibilityWith(boolean isTooltipOnRightOfAnchor) {
        if (isTooltipOnRightOfAnchor) {
            mArrowLeftView.setVisibility(VISIBLE);
            mArrowRightView.setVisibility(GONE);
        } else {
            mArrowLeftView.setVisibility(GONE);
            mArrowRightView.setVisibility(VISIBLE);
        }
    }

    private void drawArrow(View arrowView, boolean isPointingLeft) {
        final TriangleShape triangleShape =
                TriangleShape.createHorizontal(mArrowWidth, mArrowHeight, isPointingLeft);
        final ShapeDrawable arrowDrawable = new ShapeDrawable(triangleShape);
        final Paint arrowPaint = arrowDrawable.getPaint();
        arrowPaint.setColor(mColorAccentPrimary);

        final CornerPathEffect effect = new CornerPathEffect(mArrowCornerRadius);
        arrowPaint.setPathEffect(effect);

        arrowView.setBackground(arrowDrawable);
    }

    private void initViews() {
        final View contentView = LayoutInflater.from(getContext()).inflate(
                R.layout.accessibility_floating_menu_tooltip, /* root= */ this, /* attachToRoot= */
                false);

        mMessageView = contentView.findViewById(R.id.text);
        mMessageView.setMovementMethod(LinkMovementMethod.getInstance());

        mArrowLeftView = contentView.findViewById(R.id.arrow_left);
        mArrowRightView = contentView.findViewById(R.id.arrow_right);

        updateMessageView();
        updateArrowView();

        addView(contentView);
    }

    private int getAvailableTextViewWidth(boolean isOnRightOfAnchor) {
        final PointF position = mMenuViewAppearance.getMenuPosition();
        final int availableWidth = isOnRightOfAnchor
                ? mMenuViewAppearance.getMenuDraggableBounds().width() - (int) position.x
                : (int) position.x;

        return availableWidth - mArrowWidth - mArrowMargin - mTextViewMargin;
    }
}
+29 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.accessibility.floatingmenu;

import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
@@ -28,6 +29,7 @@ import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfo
import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
@@ -40,6 +42,8 @@ import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/**
@@ -52,12 +56,23 @@ class MenuInfoRepository {
    @FloatRange(from = 0.0, to = 1.0)
    private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.77f;
    private static final boolean DEFAULT_MOVE_TO_TUCKED_VALUE = false;
    private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;

    private final Context mContext;
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final OnSettingsContentsChanged mSettingsContentsCallback;
    private Position mPercentagePosition;

    @IntDef({
            MigrationPrompt.DISABLED,
            MigrationPrompt.ENABLED,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface MigrationPrompt {
        int DISABLED = 0;
        int ENABLED = 1;
    }

    private final ContentObserver mMenuTargetFeaturesContentObserver =
            new ContentObserver(mHandler) {
                @Override
@@ -99,6 +114,13 @@ class MenuInfoRepository {
                        DEFAULT_MOVE_TO_TUCKED_VALUE));
    }

    void loadMigrationTooltipVisibility(OnInfoReady<Boolean> callback) {
        callback.onReady(Settings.Secure.getIntForUser(mContext.getContentResolver(),
                ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
                DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT, UserHandle.USER_CURRENT)
                == MigrationPrompt.ENABLED);
    }

    void loadMenuPosition(OnInfoReady<Position> callback) {
        callback.onReady(mPercentagePosition);
    }
@@ -131,6 +153,13 @@ class MenuInfoRepository {
                percentagePosition.toString());
    }

    void updateMigrationTooltipVisibility(boolean visible) {
        Settings.Secure.putIntForUser(mContext.getContentResolver(),
                ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
                visible ? MigrationPrompt.ENABLED : MigrationPrompt.DISABLED,
                UserHandle.USER_CURRENT);
    }

    private Position getStartPosition() {
        final String absolutePositionString = Prefs.getString(mContext,
                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+9 −0
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.Optional;

/**
 * Controls the all touch events of the accessibility target features view{@link RecyclerView} in
 * the {@link MenuView}. And then compute the gestures' velocity for fling and spring
@@ -39,6 +41,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
    private boolean mIsDragging = false;
    private float mTouchSlop;
    private final DismissAnimationController mDismissAnimationController;
    private Optional<Runnable> mOnActionDownEnd = Optional.empty();

    MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
            DismissAnimationController dismissAnimationController) {
@@ -65,6 +68,8 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {

                mMenuAnimationController.cancelAnimations();
                mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent);

                mOnActionDownEnd.ifPresent(Runnable::run);
                break;
            case MotionEvent.ACTION_MOVE:
                if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
@@ -125,6 +130,10 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
        // Do nothing
    }

    void setOnActionDownEndListener(Runnable onActionDownEndListener) {
        mOnActionDownEnd = Optional.ofNullable(onActionDownEndListener);
    }

    /**
     * Adds a movement to the velocity tracker using raw screen coordinates.
     */
+1 −1
Original line number Diff line number Diff line
@@ -293,7 +293,7 @@ class MenuViewAppearance {
        return bounds;
    }

    private boolean isMenuOnLeftSide() {
    boolean isMenuOnLeftSide() {
        return mPercentagePosition.getPercentageX() < 0.5f;
    }

+105 −7
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static android.view.WindowInsets.Type.ime;

import static androidx.core.view.WindowInsetsCompat.Type;

import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE;
import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
@@ -27,8 +28,10 @@ import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.In

import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.StringDef;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Handler;
@@ -37,6 +40,8 @@ import android.os.UserHandle;
import android.provider.Settings;
import android.util.PluralsMessageFormatter;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
@@ -45,6 +50,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.lifecycle.Observer;

import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
@@ -58,6 +64,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and
@@ -66,11 +73,13 @@ import java.util.Map;
 * message view would be shown and allowed users to undo it.
 */
@SuppressLint("ViewConstructor")
class MenuViewLayer extends FrameLayout {
class MenuViewLayer extends FrameLayout implements
        ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener {
    private static final int SHOW_MESSAGE_DELAY_MS = 3000;

    private final WindowManager mWindowManager;
    private final MenuView mMenuView;
    private final MenuListViewTouchHandler mMenuListViewTouchHandler;
    private final MenuMessageView mMessageView;
    private final DismissView mDismissView;
    private final MenuViewAppearance mMenuViewAppearance;
@@ -79,18 +88,33 @@ class MenuViewLayer extends FrameLayout {
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final IAccessibilityFloatingMenu mFloatingMenu;
    private final DismissAnimationController mDismissAnimationController;
    private final MenuViewModel mMenuViewModel;
    private final Observer<Boolean> mMigrationTooltipObserver =
            this::onMigrationTooltipVisibilityChanged;
    private final Rect mImeInsetsRect = new Rect();
    private boolean mIsMigrationTooltipShowing;
    private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();

    @IntDef({
            LayerIndex.MENU_VIEW,
            LayerIndex.DISMISS_VIEW,
            LayerIndex.MESSAGE_VIEW,
            LayerIndex.TOOLTIP_VIEW,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface LayerIndex {
        int MENU_VIEW = 0;
        int DISMISS_VIEW = 1;
        int MESSAGE_VIEW = 2;
        int TOOLTIP_VIEW = 3;
    }

    @StringDef({
            TooltipType.MIGRATION,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface TooltipType {
        String MIGRATION = "migration";
    }

    @VisibleForTesting
@@ -125,12 +149,11 @@ class MenuViewLayer extends FrameLayout {
        mAccessibilityManager = accessibilityManager;
        mFloatingMenu = floatingMenu;

        final MenuViewModel menuViewModel = new MenuViewModel(context);
        mMenuViewModel = new MenuViewModel(context);
        mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
        mMenuView = new MenuView(context, menuViewModel, mMenuViewAppearance);
        mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
        mMenuAnimationController = mMenuView.getMenuAnimationController();
        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);

        mDismissView = new DismissView(context);
        mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
        mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -153,9 +176,9 @@ class MenuViewLayer extends FrameLayout {
            }
        });

        final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler(
                mMenuAnimationController, mDismissAnimationController);
        mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler);
        mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
                mDismissAnimationController);
        mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);

        mMessageView = new MenuMessageView(context);

@@ -210,7 +233,11 @@ class MenuViewLayer extends FrameLayout {
        super.onAttachedToWindow();

        mMenuView.show();
        setOnClickListener(this);
        setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
        mMenuViewModel.getMigrationTooltipVisibilityData().observeForever(
                mMigrationTooltipObserver);
        mMessageView.setUndoListener(view -> undo());
        mContext.registerComponentCallbacks(mDismissAnimationController);
    }
@@ -220,11 +247,31 @@ class MenuViewLayer extends FrameLayout {
        super.onDetachedFromWindow();

        mMenuView.hide();
        setOnClickListener(null);
        setOnApplyWindowInsetsListener(null);
        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
        mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver(
                mMigrationTooltipObserver);
        mHandler.removeCallbacksAndMessages(/* token= */ null);
        mContext.unregisterComponentCallbacks(mDismissAnimationController);
    }

    @Override
    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);

        if (mEduTooltipView.isPresent()) {
            final int x = (int) getX();
            final int y = (int) getY();
            inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight()));
        }
    }

    @Override
    public void onClick(View v) {
        mEduTooltipView.ifPresent(this::removeTooltip);
    }

    private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
@@ -249,6 +296,57 @@ class MenuViewLayer extends FrameLayout {
        return insets;
    }

    private void onMigrationTooltipVisibilityChanged(boolean visible) {
        mIsMigrationTooltipShowing = visible;

        if (mIsMigrationTooltipShowing) {
            mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
            mEduTooltipView.ifPresent(
                    view -> addTooltipView(view, getMigrationMessage(), TooltipType.MIGRATION));
        }
    }

    private CharSequence getMigrationMessage() {
        final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(Intent.EXTRA_COMPONENT_NAME,
                ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString());

        final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
                AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
                v -> {
                    getContext().startActivity(intent);
                    mEduTooltipView.ifPresent(this::removeTooltip);
                });

        final int textResId = R.string.accessibility_floating_button_migration_tooltip;

        return AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo);
    }

    private void addTooltipView(MenuEduTooltipView tooltipView, CharSequence message,
            CharSequence tag) {
        addView(tooltipView, LayerIndex.TOOLTIP_VIEW);

        tooltipView.show(message);
        tooltipView.setTag(tag);

        mMenuListViewTouchHandler.setOnActionDownEndListener(
                () -> mEduTooltipView.ifPresent(this::removeTooltip));
    }

    private void removeTooltip(View tooltipView) {
        if (tooltipView.getTag().equals(TooltipType.MIGRATION)) {
            mMenuViewModel.updateMigrationTooltipVisibility(/* visible= */ false);
            mIsMigrationTooltipShowing = false;
        }

        removeView(tooltipView);

        mMenuListViewTouchHandler.setOnActionDownEndListener(null);
        mEduTooltipView = Optional.empty();
    }

    private void hideMenuAndShowMessage() {
        final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
                SHOW_MESSAGE_DELAY_MS,
Loading