Merge "Fixing the announcement of menu of a11y actions when using Talkback or Switch Access." into sc-dev

This commit is contained in:
PETER LIANG
2021-07-01 00:39:43 +00:00
committed by Android (Google) Code Review
4 changed files with 323 additions and 175 deletions

View File

@@ -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);

View File

@@ -0,0 +1,141 @@
/*
* 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);
}
}

View File

@@ -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();

View File

@@ -0,0 +1,170 @@
/*
* 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();
}
}