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

Commit d2f7022a authored by jasonwshsu's avatar jasonwshsu
Browse files

Maintain the position of accessibility floating menu

* Use percentage of X-aixs and Y-axis as last position, so it could be
restored at approximately position when device screen size changed.
* Fine-tune AccessibilityFloatingMenuViewTest

Bug: 183342667
Test: atest AccessibilityFloatingMenuViewTest PositionTest
Change-Id: I58ed608a03bc20f15a9d0852c95907107516c9ba
parent f142b5a3
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -74,7 +74,8 @@ public final class Prefs {
            Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP,
            Key.HAS_SEEN_REVERSE_BOTTOM_SHEET,
            Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT,
            Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP
            Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
            Key.ACCESSIBILITY_FLOATING_MENU_POSITION
    })
    // TODO: annotate these with their types so {@link PrefsCommandLine} can know how to set them
    public @interface Key {
@@ -125,6 +126,7 @@ public final class Prefs {
        String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount";
        String HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP =
                "HasSeenAccessibilityFloatingMenuDockTooltip";
        String ACCESSIBILITY_FLOATING_MENU_POSITION = "AccessibilityFloatingMenuPosition";
    }

    public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
+41 −5
Original line number Diff line number Diff line
@@ -28,12 +28,16 @@ import static com.android.systemui.Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MEN
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;

import android.annotation.FloatRange;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;

import androidx.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;
@@ -44,7 +48,13 @@ import com.android.systemui.Prefs;
public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
    private static final int DEFAULT_FADE_EFFECT_IS_ENABLED = 1;
    private static final int DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED = 0;
    @FloatRange(from = 0.0, to = 1.0)
    private static final float DEFAULT_OPACITY_VALUE = 0.55f;
    @FloatRange(from = 0.0, to = 1.0)
    private static final float DEFAULT_POSITION_X_PERCENT = 1.0f;
    @FloatRange(from = 0.0, to = 1.0)
    private static final float DEFAULT_POSITION_Y_PERCENT = 0.8f;

    private final Context mContext;
    private final AccessibilityFloatingMenuView mMenuView;
    private final MigrationTooltipView mMigrationTooltipView;
@@ -85,7 +95,10 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
            };

    public AccessibilityFloatingMenu(Context context) {
        this(context, new AccessibilityFloatingMenuView(context));
        mContext = context;
        mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
        mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
        mDockTooltipView = new DockTooltipView(mContext, mMenuView);
    }

    @VisibleForTesting
@@ -113,7 +126,7 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
                getOpacityValue(mContext));
        mMenuView.setSizeType(getSizeType(mContext));
        mMenuView.setShapeType(getShapeType(mContext));
        mMenuView.setOnDragEndListener(this::showDockTooltipIfNecessary);
        mMenuView.setOnDragEndListener(this::onDragEnd);

        showMigrationTooltipIfNecessary();

@@ -127,12 +140,25 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
        }

        mMenuView.hide();
        mMenuView.setOnDragEndListener(null);
        mMigrationTooltipView.hide();
        mDockTooltipView.hide();

        unregisterContentObservers();
    }

    @NonNull
    private Position getPosition(Context context) {
        final String absolutePositionString = Prefs.getString(context,
                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);

        if (TextUtils.isEmpty(absolutePositionString)) {
            return new Position(DEFAULT_POSITION_X_PERCENT, DEFAULT_POSITION_Y_PERCENT);
        } else {
            return Position.fromString(absolutePositionString);
        }
    }

    // Migration tooltip was the android S feature. It's just used on the Android version from R
    // to S. In addition, it only shows once.
    private void showMigrationTooltipIfNecessary() {
@@ -150,18 +176,28 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
                DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
    }

    private void onDragEnd(Position position) {
        savePosition(mContext, position);
        showDockTooltipIfNecessary(mContext);
    }

    private void savePosition(Context context, Position position) {
        Prefs.putString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
                position.toString());
    }

    /**
     * Shows tooltip when user drags accessibility floating menu for the first time.
     */
    private void showDockTooltipIfNecessary() {
        if (!Prefs.get(mContext).getBoolean(
    private void showDockTooltipIfNecessary(Context context) {
        if (!Prefs.get(context).getBoolean(
                HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, false)) {
            // if the menu is an oval, the user has already dragged it out, so show the tooltip.
            if (mMenuView.isOvalShape()) {
                mDockTooltipView.show();
            }

            Prefs.putBoolean(mContext, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
            Prefs.putBoolean(context, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
        }
    }

+51 −30
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -85,7 +86,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    private static final int FADE_EFFECT_DURATION_MS = 3000;
    private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
    private static final int MIN_WINDOW_Y = 0;
    private static final float LOCATION_Y_PERCENTAGE = 0.8f;

    private static final int ANIMATION_START_OFFSET = 600;
    private static final int ANIMATION_DURATION_MS = 600;
@@ -97,7 +97,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    private boolean mIsDragging = false;
    private boolean mImeVisibility;
    @Alignment
    private int mAlignment = Alignment.RIGHT;
    private int mAlignment;
    @SizeType
    private int mSizeType = SizeType.SMALL;
    @VisibleForTesting
@@ -105,7 +105,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    int mShapeType = ShapeType.OVAL;
    private int mTemporaryShapeType;
    @RadiusType
    private int mRadiusType = RadiusType.LEFT_HALF_OVAL;
    private int mRadiusType;
    private int mMargin;
    private int mPadding;
    private int mScreenHeight;
@@ -118,7 +118,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    private int mRelativeToPointerDownX;
    private int mRelativeToPointerDownY;
    private float mRadius;
    private float mPercentageY = LOCATION_Y_PERCENTAGE;
    private final Position mPosition;
    private float mSquareScaledTouchSlop;
    private final Configuration mLastConfiguration;
    private Optional<OnDragEndListener> mOnDragEndListener = Optional.empty();
@@ -182,25 +182,35 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    interface OnDragEndListener {

        /**
         * Invoked when the floating menu has dragged end.
         * Called when a drag is completed.
         *
         * @param position Stores information about the position
         */
        void onDragEnd();
        void onDragEnd(Position position);
    }

    public AccessibilityFloatingMenuView(Context context) {
        this(context, new RecyclerView(context));
    public AccessibilityFloatingMenuView(Context context, @NonNull Position position) {
        this(context, position, new RecyclerView(context));
    }

    @VisibleForTesting
    AccessibilityFloatingMenuView(Context context,
    AccessibilityFloatingMenuView(Context context, @NonNull Position position,
            RecyclerView listView) {
        super(context);

        mListView = listView;
        mWindowManager = context.getSystemService(WindowManager.class);
        mCurrentLayoutParams = createDefaultLayoutParams();
        mAdapter = new AccessibilityTargetAdapter(mTargets);
        mUiHandler = createUiHandler();
        mPosition = position;
        mAlignment = transformToAlignment(mPosition.getPercentageX());
        mRadiusType = (mAlignment == Alignment.RIGHT)
                ? RadiusType.LEFT_HALF_OVAL
                : RadiusType.RIGHT_HALF_OVAL;

        updateDimensions();

        mCurrentLayoutParams = createDefaultLayoutParams();

        mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
        mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
@@ -213,10 +223,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        mDragAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mAlignment = calculateCurrentAlignment();
                mPercentageY = calculateCurrentPercentageY();
                mPosition.update(transformCurrentPercentageXToEdge(),
                        calculateCurrentPercentageY());
                mAlignment = transformToAlignment(mPosition.getPercentageX());

                updateLocationWith(mAlignment, mPercentageY);
                updateLocationWith(mPosition);

                updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);

@@ -227,13 +238,13 @@ public class AccessibilityFloatingMenuView extends FrameLayout

                fadeOut();

                mOnDragEndListener.ifPresent(OnDragEndListener::onDragEnd);
                mOnDragEndListener.ifPresent(
                        onDragEndListener -> onDragEndListener.onDragEnd(mPosition));
            }
        });

        mLastConfiguration = new Configuration(getResources().getConfiguration());

        updateDimensions();
        initListView();
        updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
    }
@@ -423,7 +434,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        updateRadiusWith(newSizeType, mRadiusType, mTargets.size());

        // When the icon sized changed, the menu size and location will be impacted.
        updateLocationWith(mAlignment, mPercentageY);
        updateLocationWith(mPosition);
        updateScrollModeWith(hasExceededMaxLayoutHeight());
        updateOffsetWith(mShapeType, mAlignment);
        setSystemGestureExclusion();
@@ -446,14 +457,14 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        fadeOut();
    }

    public void setOnDragEndListener(OnDragEndListener onDragListener) {
        mOnDragEndListener = Optional.ofNullable(onDragListener);
    public void setOnDragEndListener(OnDragEndListener onDragEndListener) {
        mOnDragEndListener = Optional.ofNullable(onDragEndListener);
    }

    void startTranslateXAnimation() {
        fadeIn();

        final float toXValue = mAlignment == Alignment.RIGHT
        final float toXValue = (mAlignment == Alignment.RIGHT)
                ? ANIMATION_TO_X_VALUE
                : -ANIMATION_TO_X_VALUE;
        final TranslateAnimation animation =
@@ -581,7 +592,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        final boolean currentImeVisibility = insets.isVisible(ime());
        if (currentImeVisibility != mImeVisibility) {
            mImeVisibility = currentImeVisibility;
            updateLocationWith(mAlignment, mPercentageY);
            updateLocationWith(mPosition);
        }

        return insets;
@@ -697,8 +708,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        params.receiveInsetsIgnoringZOrder = true;
        params.windowAnimations = android.R.style.Animation_Translucent;
        params.gravity = Gravity.START | Gravity.TOP;
        params.x = getMaxWindowX();
        params.y = (int) (getMaxWindowY() * mPercentageY);
        params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
//        params.y = (int) (mPosition.getPercentageY() * getMaxWindowY());
        final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
        params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
        updateAccessibilityTitle(params);
        return params;
    }
@@ -716,7 +729,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        updateItemViewWith(mSizeType);
        updateColor();
        updateStrokeWith(newConfig.uiMode, mAlignment);
        updateLocationWith(mAlignment, mPercentageY);
        updateLocationWith(mPosition);
        updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
        updateScrollModeWith(hasExceededMaxLayoutHeight());
        setSystemGestureExclusion();
@@ -765,9 +778,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    /**
     * Updates the floating menu to be fixed at the side of the screen.
     */
    private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
        mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
        final int currentLayoutY = (int) (percentageCurrentY * getMaxWindowY());
    private void updateLocationWith(Position position) {
        final @Alignment int alignment = transformToAlignment(position.getPercentageX());
        mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
        final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY());
        mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
        mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
    }
@@ -861,10 +875,17 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    }

    @Alignment
    private int calculateCurrentAlignment() {
        return mCurrentLayoutParams.x >= ((getMinWindowX() + getMaxWindowX()) / 2)
                ? Alignment.RIGHT
                : Alignment.LEFT;
    private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) {
        return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT;
    }

    private float transformCurrentPercentageXToEdge() {
        final float percentageX = calculateCurrentPercentageX();
        return (percentageX < 0.5) ? 0.0f : 1.0f;
    }

    private float calculateCurrentPercentageX() {
        return mCurrentLayoutParams.x / (float) getMaxWindowX();
    }

    private float calculateCurrentPercentageY() {
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.annotation.FloatRange;
import android.text.TextUtils;

/**
 * Stores information about the position, which includes percentage of X-axis of the screen,
 * percentage of Y-axis of the screen.
 */
public class Position {

    private static final char STRING_SEPARATOR = ',';
    private static final TextUtils.SimpleStringSplitter sStringCommaSplitter =
            new TextUtils.SimpleStringSplitter(STRING_SEPARATOR);

    private float mPercentageX;
    private float mPercentageY;

    /**
     * Creates a {@link Position} from a encoded string described in {@link #toString()}.
     *
     * @param positionString A string conform to the format described in {@link #toString()}
     * @return A {@link Position} with the given value retrieved from {@code absolutePositionString}
     * @throws IllegalArgumentException If {@code positionString} does not conform to the format
     *                                  described in {@link #toString()}
     */
    public static Position fromString(String positionString) {
        sStringCommaSplitter.setString(positionString);
        if (sStringCommaSplitter.hasNext()) {
            final float percentageX = Float.parseFloat(sStringCommaSplitter.next());
            final float percentageY = Float.parseFloat(sStringCommaSplitter.next());
            return new Position(percentageX, percentageY);
        }

        throw new IllegalArgumentException(
                "Invalid Position string: " + positionString);
    }

    Position(float percentageX, float percentageY) {
        update(percentageX, percentageY);
    }

    @Override
    public String toString() {
        return mPercentageX + ", " + mPercentageY;
    }

    /**
     * Updates the position with {@code percentageX} and {@code percentageY}.
     *
     * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0.
     * @param percentageY the new percentage of Y-axis of the screen, from 0.0 to 1.0.
     */
    public void update(@FloatRange(from = 0.0, to = 1.0) float percentageX,
            @FloatRange(from = 0.0, to = 1.0) float percentageY) {
        mPercentageX = percentageX;
        mPercentageY = percentageY;
    }

    public float getPercentageX() {
        return mPercentageX;
    }

    public float getPercentageY() {
        return mPercentageY;
    }
}
+8 −9
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import android.content.Context;
import android.testing.AndroidTestingRunner;
@@ -31,15 +30,16 @@ import android.view.accessibility.AccessibilityManager;

import androidx.test.filters.SmallTest;

import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.SysuiTestCase;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.ArrayList;
import java.util.List;
@@ -50,6 +50,9 @@ import java.util.List;
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuTest extends SysuiTestCase {

    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    @Mock
    private AccessibilityManager mAccessibilityManager;

@@ -58,18 +61,14 @@ public class AccessibilityFloatingMenuTest extends SysuiTestCase {

    @Before
    public void initMenu() {
        MockitoAnnotations.initMocks(this);

        final List<AccessibilityTarget> mTargets = new ArrayList<>();
        mTargets.add(mock(AccessibilityTarget.class));

        final List<String> assignedTargets = new ArrayList<>();
        mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
        assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
        doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
                anyInt());

        mMenuView = new AccessibilityFloatingMenuView(mContext);
        final Position position = new Position(0, 0);
        mMenuView = new AccessibilityFloatingMenuView(mContext, position);
        mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
    }

Loading