diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index e891e5b64b3d0..17818cd9c7ee0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java new file mode 100644 index 0000000000000..93b0676b49300 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompat.java @@ -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 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); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java index 09e6940acd432..06e27b5dbf486 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java @@ -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 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(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java new file mode 100644 index 0000000000000..dae436427319e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java @@ -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(); + } +}