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

Commit 5fec1068 authored by Peter_Liang's avatar Peter_Liang
Browse files

Fix the accessibility floating menu overlap with the keyboard.

Solution:
Use WindowInset to get the ime height to avoid overlapping each other.

Bug: 175364596
Test: atest AccessibilityFloatingMenuViewTest
Change-Id: I01efda29d0bd91a580d2595ec238d1e459c60e9e
parent 75dc1f16
Loading
Loading
Loading
Loading
+42 −1
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.accessibility.floatingmenu;

import static android.util.MathUtils.constrain;
import static android.util.MathUtils.sq;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;

import static java.util.Objects.requireNonNull;

@@ -29,6 +31,7 @@ import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -42,7 +45,9 @@ import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.Animation;
@@ -90,6 +95,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
    private boolean mIsShowing;
    private boolean mIsDownInEnlargedTouchArea;
    private boolean mIsDragging = false;
    private boolean mImeVisibility;
    @Alignment
    private int mAlignment = Alignment.RIGHT;
    @SizeType
@@ -369,6 +375,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout

        mIsShowing = true;
        mWindowManager.addView(this, mCurrentLayoutParams);

        setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
        setSystemGestureExclusion();
    }

@@ -379,6 +387,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout

        mIsShowing = false;
        mWindowManager.removeView(this);

        setOnApplyWindowInsetsListener(null);
        setSystemGestureExclusion();
    }

@@ -565,6 +575,16 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        return mListView.dispatchTouchEvent(event);
    }

    private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
        final boolean currentImeVisibility = insets.isVisible(ime());
        if (currentImeVisibility != mImeVisibility) {
            mImeVisibility = currentImeVisibility;
            updateLocationWith(mAlignment, mPercentageY);
        }

        return insets;
    }

    private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
        return (side == Alignment.RIGHT && currentRawX > downX)
                || (side == Alignment.LEFT && downX > currentRawX);
@@ -658,6 +678,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
                PixelFormat.TRANSLUCENT);
        params.receiveInsetsIgnoringZOrder = true;
        params.windowAnimations = android.R.style.Animation_Translucent;
        params.gravity = Gravity.START | Gravity.TOP;
        params.x = getMaxWindowX();
@@ -728,10 +749,30 @@ public class AccessibilityFloatingMenuView extends FrameLayout
     */
    private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
        mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
        mCurrentLayoutParams.y = (int) (percentageCurrentY * getMaxWindowY());
        final int currentLayoutY = (int) (percentageCurrentY * getMaxWindowY());
        mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
        mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
    }

    /**
     * Gets the moving interval to not overlap between the keyboard and menu view.
     *
     * @return the moving interval if they overlap each other, otherwise 0.
     */
    private int getInterval() {
        if (!mImeVisibility) {
            return 0;
        }

        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
        final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
                ime() | navigationBars());
        final int imeY = mScreenHeight - imeInsets.bottom;
        final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight();

        return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
    }

    private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
        final float halfWidth = getLayoutWidth() / 2.0f;
        final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
+82 −2
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.accessibility.floatingmenu;

import static android.view.View.OVER_SCROLL_ALWAYS;
import static android.view.View.OVER_SCROLL_NEVER;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.navigationBars;

import static com.google.common.truth.Truth.assertThat;

@@ -31,9 +33,11 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
@@ -43,7 +47,9 @@ import android.testing.TestableLooper;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.annotation.NonNull;
@@ -79,12 +85,17 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
    @Mock
    private ViewPropertyAnimator mAnimator;

    @Mock
    private WindowMetrics mWindowMetrics;

    private MotionEvent mInterceptMotionEvent;

    private RecyclerView mListView;

    private Rect mAvailableBounds = new Rect(100, 200, 300, 400);

    private int mScreenHeight;
    private int mMenuWindowHeight;
    private int mMenuHalfWidth;
    private int mMenuHalfHeight;
    private int mScreenHalfWidth;
@@ -117,12 +128,13 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
        final int menuWidth = padding * 2 + iconWidthHeight;
        final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
        final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
        final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
        mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
        mMenuHalfWidth = menuWidth / 2;
        mMenuHalfHeight = menuHeight / 2;
        mScreenHalfWidth = screenWidth / 2;
        mScreenHalfHeight = screenHeight / 2;
        mScreenHalfHeight = mScreenHeight / 2;
        mMaxWindowX = screenWidth - margin - menuWidth;
        mMenuWindowHeight = menuHeight + margin * 2;
    }

    @Test
@@ -464,12 +476,80 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
        assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER);
    }

    @Test
    public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() {
        final int offset = 200;

        showMenuWithLatestStatus();
        final WindowInsets imeInset = fakeImeInsetWith(offset);
        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
        when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
        final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y - offset;
        mMenuView.dispatchApplyWindowInsets(imeInset);

        assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
    }

    @Test
    public void hideIme_onMenuViewShifted_menuViewMovedBack() {
        final int offset = 200;
        showMenuWithLatestStatus();
        final WindowInsets imeInset = fakeImeInsetWith(offset);
        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
        when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
        final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y;
        mMenuView.dispatchApplyWindowInsets(imeInset);

        mMenuView.dispatchApplyWindowInsets(
                new WindowInsets.Builder().setVisible(ime(), false).build());

        assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
    }

    @Test
    public void showMenuAndIme_withHigherIme_alignScreenTopEdge() {
        final int offset = 99999;

        showMenuWithLatestStatus();
        final WindowInsets imeInset = fakeImeInsetWith(offset);
        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
        when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
        mMenuView.dispatchApplyWindowInsets(imeInset);

        assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0);
    }

    @After
    public void tearDown() {
        mInterceptMotionEvent = null;
        mMotionEventHelper.recycleEvents();
    }

    private void showMenuWithLatestStatus() {
        mMenuView.show();
        mMenuView.onTargetsChanged(mTargets);
        mMenuView.setSizeType(0);
        mMenuView.setShapeType(0);
    }

    /**
     * Based on the current menu status, fake the ime inset component {@link WindowInsets} used
     * for testing.
     *
     * @param offset is used for the y-axis position of ime higher than the y-axis position of menu.
     * @return the ime inset
     */
    private WindowInsets fakeImeInsetWith(int offset) {
        // Ensure the keyboard has overlapped on the menu view.
        final int fakeImeHeight =
                mScreenHeight - (mMenuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;

        return new WindowInsets.Builder()
                .setVisible(ime() | navigationBars(), true)
                .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight))
                .build();
    }

    private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
        TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
            super(context, listView);