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

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

Merge "Fixing the announcement of menu of a11y actions when using Talkback or...

Merge "Fixing the announcement of menu of a11y actions when using Talkback or Switch Access." into sc-dev
parents 286f70df 32b05d40
Loading
Loading
Loading
Loading
+11 −88
Original line number Diff line number Diff line
@@ -39,7 +39,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
@@ -50,8 +49,6 @@ 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;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
@@ -59,8 +56,10 @@ import android.widget.FrameLayout;

import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;

import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
@@ -332,54 +331,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        // Do Nothing
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        setupAccessibilityActions(info);
    }

    @Override
    public boolean performAccessibilityAction(int action, Bundle arguments) {
        fadeIn();

        final Rect bounds = getAvailableBounds();
        if (action == R.id.action_move_top_left) {
            setShapeType(ShapeType.OVAL);
            snapToLocation(bounds.left, bounds.top);
            return true;
        }

        if (action == R.id.action_move_top_right) {
            setShapeType(ShapeType.OVAL);
            snapToLocation(bounds.right, bounds.top);
            return true;
        }

        if (action == R.id.action_move_bottom_left) {
            setShapeType(ShapeType.OVAL);
            snapToLocation(bounds.left, bounds.bottom);
            return true;
        }

        if (action == R.id.action_move_bottom_right) {
            setShapeType(ShapeType.OVAL);
            snapToLocation(bounds.right, bounds.bottom);
            return true;
        }

        if (action == R.id.action_move_to_edge_and_hide) {
            setShapeType(ShapeType.HALF_OVAL);
            return true;
        }

        if (action == R.id.action_move_out_edge_and_show) {
            setShapeType(ShapeType.OVAL);
            return true;
        }

        return super.performAccessibilityAction(action, arguments);
    }

    void show() {
        if (isShowing()) {
            return;
@@ -526,43 +477,6 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS);
    }

    private void setupAccessibilityActions(AccessibilityNodeInfo info) {
        final Resources res = mContext.getResources();
        final AccessibilityAction moveTopLeft =
                new AccessibilityAction(R.id.action_move_top_left,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_top_left));
        info.addAction(moveTopLeft);

        final AccessibilityAction moveTopRight =
                new AccessibilityAction(R.id.action_move_top_right,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_top_right));
        info.addAction(moveTopRight);

        final AccessibilityAction moveBottomLeft =
                new AccessibilityAction(R.id.action_move_bottom_left,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_bottom_left));
        info.addAction(moveBottomLeft);

        final AccessibilityAction moveBottomRight =
                new AccessibilityAction(R.id.action_move_bottom_right,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_bottom_right));
        info.addAction(moveBottomRight);

        final int moveEdgeId = mShapeType == ShapeType.OVAL
                ? R.id.action_move_to_edge_and_hide
                : R.id.action_move_out_edge_and_show;
        final int moveEdgeTextResId = mShapeType == ShapeType.OVAL
                ? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half
                : R.string.accessibility_floating_button_action_move_out_edge_and_show;
        final AccessibilityAction moveToOrOutEdge =
                new AccessibilityAction(moveEdgeId, res.getString(moveEdgeTextResId));
        info.addAction(moveToOrOutEdge);
    }

    private boolean onTouched(MotionEvent event) {
        final int action = event.getAction();
        final int currentX = (int) event.getX();
@@ -685,6 +599,15 @@ public class AccessibilityFloatingMenuView extends FrameLayout
        mListView.setLayoutManager(layoutManager);
        mListView.addOnItemTouchListener(this);
        mListView.animate().setInterpolator(new OvershootInterpolator());
        mListView.setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView) {
            @NonNull
            @Override
            public AccessibilityDelegateCompat getItemDelegate() {
                return new ItemDelegateCompat(this,
                        AccessibilityFloatingMenuView.this);
            }
        });

        updateListViewWith(mLastConfiguration);

        addView(mListView);
+141 −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.content.res.Resources;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;

import com.android.systemui.R;
import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;

import java.lang.ref.WeakReference;

/**
 * An accessibility item delegate for the individual items of the list view
 * {@link AccessibilityFloatingMenuView}.
 */
final class ItemDelegateCompat extends RecyclerViewAccessibilityDelegate.ItemDelegate {
    private final WeakReference<AccessibilityFloatingMenuView> mMenuViewRef;

    ItemDelegateCompat(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
            AccessibilityFloatingMenuView menuView) {
        super(recyclerViewDelegate);
        this.mMenuViewRef = new WeakReference<>(menuView);
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
        super.onInitializeAccessibilityNodeInfo(host, info);

        if (mMenuViewRef.get() == null) {
            return;
        }
        final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();

        final Resources res = menuView.getResources();
        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_top_left));
        info.addAction(moveTopLeft);

        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                        R.id.action_move_top_right,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_top_right));
        info.addAction(moveTopRight);

        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                        R.id.action_move_bottom_left,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_bottom_left));
        info.addAction(moveBottomLeft);

        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                        R.id.action_move_bottom_right,
                        res.getString(
                                R.string.accessibility_floating_button_action_move_bottom_right));
        info.addAction(moveBottomRight);

        final int moveEdgeId = menuView.isOvalShape()
                ? R.id.action_move_to_edge_and_hide
                : R.id.action_move_out_edge_and_show;
        final int moveEdgeTextResId = menuView.isOvalShape()
                ? R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half
                : R.string.accessibility_floating_button_action_move_out_edge_and_show;
        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
                        res.getString(moveEdgeTextResId));
        info.addAction(moveToOrOutEdge);
    }

    @Override
    public boolean performAccessibilityAction(View host, int action, Bundle args) {
        if (mMenuViewRef.get() == null) {
            return super.performAccessibilityAction(host, action, args);
        }
        final AccessibilityFloatingMenuView menuView = mMenuViewRef.get();

        menuView.fadeIn();

        final Rect bounds = menuView.getAvailableBounds();
        if (action == R.id.action_move_top_left) {
            menuView.setShapeType(ShapeType.OVAL);
            menuView.snapToLocation(bounds.left, bounds.top);
            return true;
        }

        if (action == R.id.action_move_top_right) {
            menuView.setShapeType(ShapeType.OVAL);
            menuView.snapToLocation(bounds.right, bounds.top);
            return true;
        }

        if (action == R.id.action_move_bottom_left) {
            menuView.setShapeType(ShapeType.OVAL);
            menuView.snapToLocation(bounds.left, bounds.bottom);
            return true;
        }

        if (action == R.id.action_move_bottom_right) {
            menuView.setShapeType(ShapeType.OVAL);
            menuView.snapToLocation(bounds.right, bounds.bottom);
            return true;
        }

        if (action == R.id.action_move_to_edge_and_hide) {
            menuView.setShapeType(ShapeType.HALF_OVAL);
            return true;
        }

        if (action == R.id.action_move_out_edge_and_show) {
            menuView.setShapeType(ShapeType.OVAL);
            return true;
        }

        return super.performAccessibilityAction(host, action, args);
    }
}
+1 −87
Original line number Diff line number Diff line
@@ -38,7 +38,6 @@ 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.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.testing.AndroidTestingRunner;
@@ -49,7 +48,6 @@ 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;
import androidx.recyclerview.widget.RecyclerView;
@@ -87,7 +85,6 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
    private final List<AccessibilityTarget> mTargets = new ArrayList<>(
            Collections.singletonList(mock(AccessibilityTarget.class)));

    private final Rect mAvailableBounds = new Rect(100, 200, 300, 400);
    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);

    @Mock
@@ -144,6 +141,7 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {

    @Test
    public void initListView_success() {
        assertThat(mListView.getCompatAccessibilityDelegate()).isNotNull();
        assertThat(mMenuView.getChildCount()).isEqualTo(1);
    }

@@ -369,90 +367,6 @@ public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
        assertThat(menuView.mShapeType).isEqualTo(/* halfOval */ 1);
    }

    @Test
    public void getAccessibilityActionList_matchResult() {
        final AccessibilityNodeInfo info = new AccessibilityNodeInfo();

        mMenuView.onInitializeAccessibilityNodeInfo(info);

        assertThat(info.getActionList().size()).isEqualTo(5);
    }

    @Test
    public void accessibilityActionMove_halfOval_moveTopLeft_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveTopLeftAction =
                mMenuView.performAccessibilityAction(R.id.action_move_top_left, null);

        assertThat(moveTopLeftAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
    }

    @Test
    public void accessibilityActionMove_halfOval_moveTopRight_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveTopRightAction =
                mMenuView.performAccessibilityAction(R.id.action_move_top_right, null);

        assertThat(moveTopRightAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
    }

    @Test
    public void accessibilityActionMove_halfOval_moveBottomLeft_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveBottomLeftAction =
                mMenuView.performAccessibilityAction(R.id.action_move_bottom_left, null);

        assertThat(moveBottomLeftAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
    }

    @Test
    public void accessibilityActionMove_halfOval_moveBottomRight_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveBottomRightAction =
                mMenuView.performAccessibilityAction(R.id.action_move_bottom_right, null);

        assertThat(moveBottomRightAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
    }

    @Test
    public void accessibilityActionMove_halfOval_moveOutEdgeAndShow_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveOutEdgeAndShowAction =
                mMenuView.performAccessibilityAction(R.id.action_move_out_edge_and_show, null);

        assertThat(moveOutEdgeAndShowAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
    }

    @Test
    public void setupAccessibilityActions_oval_hasActionMoveToEdgeAndHide() {
        final AccessibilityNodeInfo info = new AccessibilityNodeInfo();
        mMenuView.setShapeType(/* ovalShape */ 0);

        mMenuView.onInitializeAccessibilityNodeInfo(info);

        assertThat(info.getActionList().stream().anyMatch(
                action -> action.getId() == R.id.action_move_to_edge_and_hide)).isTrue();
    }

    @Test
    public void onTargetsChanged_exceedAvailableHeight_overScrollAlways() {
        doReturn(true).when(mMenuView).hasExceededMaxLayoutHeight();
+170 −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 static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
import androidx.test.filters.SmallTest;

import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;

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

/** Tests for {@link ItemDelegateCompat}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class ItemDelegateCompatTest extends SysuiTestCase {
    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    @Mock
    private WindowManager mWindowManager;

    private RecyclerView mListView;
    private AccessibilityFloatingMenuView mMenuView;
    private ItemDelegateCompat mItemDelegateCompat;
    private final Rect mAvailableBounds = new Rect(100, 200, 300, 400);
    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);

    @Before
    public void setUp() {
        final WindowManager wm = mContext.getSystemService(WindowManager.class);
        doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                mWindowManager).getMaximumWindowMetrics();
        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);

        mListView = new RecyclerView(mContext);
        mMenuView =
                spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
        mItemDelegateCompat =
                new ItemDelegateCompat(new RecyclerViewAccessibilityDelegate(mListView), mMenuView);
    }

    @Test
    public void getAccessibilityActionList_matchResult() {
        final AccessibilityNodeInfoCompat info =
                new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());

        mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);

        assertThat(info.getActionList().size()).isEqualTo(5);
    }

    @Test
    public void performAccessibilityMoveTopLeftAction_halfOval_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveTopLeftAction =
                mItemDelegateCompat.performAccessibilityAction(mListView, R.id.action_move_top_left,
                        null);

        assertThat(moveTopLeftAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
    }

    @Test
    public void performAccessibilityMoveTopRightAction_halfOval_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveTopRightAction =
                mItemDelegateCompat.performAccessibilityAction(mListView,
                        R.id.action_move_top_right, null);

        assertThat(moveTopRightAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
    }

    @Test
    public void performAccessibilityMoveBottomLeftAction_halfOval_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveBottomLeftAction =
                mItemDelegateCompat.performAccessibilityAction(mListView,
                        R.id.action_move_bottom_left, null);

        assertThat(moveBottomLeftAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
    }

    @Test
    public void performAccessibilityMoveBottomRightAction_halfOval_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveBottomRightAction =
                mItemDelegateCompat.performAccessibilityAction(mListView,
                        R.id.action_move_bottom_right, null);

        assertThat(moveBottomRightAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
    }

    @Test
    public void performAccessibilityMoveOutEdgeAction_halfOval_success() {
        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
        mMenuView.setShapeType(/* halfOvalShape */ 1);

        final boolean moveOutEdgeAndShowAction =
                mItemDelegateCompat.performAccessibilityAction(mListView,
                        R.id.action_move_out_edge_and_show, null);

        assertThat(moveOutEdgeAndShowAction).isTrue();
        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
    }

    @Test
    public void setupAccessibilityActions_oval_hasActionMoveToEdgeAndHide() {
        final AccessibilityNodeInfoCompat info =
                new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
        mMenuView.setShapeType(/* ovalShape */ 0);

        mItemDelegateCompat.onInitializeAccessibilityNodeInfo(mListView, info);

        assertThat(info.getActionList().stream().anyMatch(
                action -> action.getId() == R.id.action_move_to_edge_and_hide)).isTrue();
    }
}