diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index a9ab2cf7f3c83..d6d0399035054 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -99,6 +99,30 @@ class MenuAnimationController { } } + void moveToTopLeftPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top)); + } + + void moveToTopRightPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top)); + } + + void moveToBottomLeftPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom)); + } + + void moveToBottomRightPosition() { + mIsMovedToEdge = false; + final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom)); + } + void moveAndPersistPosition(PointF position) { moveToPosition(position); mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java new file mode 100644 index 0000000000000..e69a24810fdc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 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 androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS; +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS; + +import android.content.res.Resources; +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; + +/** + * An accessibility item delegate for the individual items of the list view in the + * {@link MenuView}. + */ +class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate { + private final MenuAnimationController mAnimationController; + + MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate, + MenuAnimationController animationController) { + super(recyclerViewDelegate); + mAnimationController = animationController; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + final Resources res = host.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 = mAnimationController.isMovedToEdge() + ? R.id.action_move_out_edge_and_show + : R.id.action_move_to_edge_and_hide; + final int moveEdgeTextResId = mAnimationController.isMovedToEdge() + ? R.string.accessibility_floating_button_action_move_out_edge_and_show + : R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half; + 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 (action == ACTION_ACCESSIBILITY_FOCUS) { + mAnimationController.fadeInNowIfEnabled(); + } + + if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) { + mAnimationController.fadeOutIfEnabled(); + } + + if (action == R.id.action_move_top_left) { + mAnimationController.moveToTopLeftPosition(); + return true; + } + + if (action == R.id.action_move_top_right) { + mAnimationController.moveToTopRightPosition(); + return true; + } + + if (action == R.id.action_move_bottom_left) { + mAnimationController.moveToBottomLeftPosition(); + return true; + } + + if (action == R.id.action_move_bottom_right) { + mAnimationController.moveToBottomRightPosition(); + return true; + } + + if (action == R.id.action_move_to_edge_and_hide) { + mAnimationController.moveToEdgeAndHide(); + return true; + } + + if (action == R.id.action_move_out_edge_and_show) { + mAnimationController.moveOutEdgeAndShow(); + return true; + } + + return super.performAccessibilityAction(host, action, args); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 7f43e62519cbe..15d139cf15da5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -28,9 +28,12 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; @@ -72,10 +75,15 @@ class MenuView extends FrameLayout implements mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); - final MenuListViewTouchHandler menuListViewTouchHandler = - new MenuListViewTouchHandler(mMenuAnimationController); - addOnItemTouchListenerToList(menuListViewTouchHandler); - + mTargetFeaturesView.setAccessibilityDelegateCompat( + new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) { + @NonNull + @Override + public AccessibilityDelegateCompat getItemDelegate() { + return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, + mMenuAnimationController); + } + }); setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); // Avoid drawing out of bounds of the parent view setClipToOutline(true); @@ -270,6 +278,7 @@ class MenuView extends FrameLayout implements void loadLayoutResources() { mMenuViewAppearance.update(); + mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription()); setBackground(mMenuViewAppearance.getMenuBackground()); setElevation(mMenuViewAppearance.getMenuElevation()); onItemSizeChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java index 2ff0703458bd3..034e96a13029f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java @@ -64,6 +64,7 @@ class MenuViewAppearance { private int mElevation; private float[] mRadii; private Drawable mBackgroundDrawable; + private String mContentDescription; @IntDef({ SMALL, @@ -108,6 +109,8 @@ class MenuViewAppearance { final Drawable drawable = mRes.getDrawable(R.drawable.accessibility_floating_menu_background); mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable}); + mContentDescription = mRes.getString( + com.android.internal.R.string.accessibility_select_shortcut_menu_title); } void setSizeType(int sizeType) { @@ -157,6 +160,10 @@ class MenuViewAppearance { + draggableBounds.height() * mPercentagePosition.getPercentageY()); } + String getContentDescription() { + return mContentDescription; + } + Drawable getMenuBackground() { return mBackgroundDrawable; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java new file mode 100644 index 0000000000000..bf6d574a0f677 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 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 androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS; +import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link MenuItemAccessibilityDelegate}. */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private RecyclerView mStubListView; + private MenuView mMenuView; + private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; + private MenuAnimationController mMenuAnimationController; + private final Rect mDraggableBounds = new Rect(100, 200, 300, 400); + + @Before + public void setUp() { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + + final int halfScreenHeight = + stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2; + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView.setTranslationY(halfScreenHeight); + + doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds(); + mStubListView = new RecyclerView(mContext); + mMenuAnimationController = spy(new MenuAnimationController(mMenuView)); + mMenuItemAccessibilityDelegate = + new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate( + mStubListView), mMenuAnimationController); + } + + @Test + public void getAccessibilityActionList_matchSize() { + final AccessibilityNodeInfoCompat info = + new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); + + mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); + + assertThat(info.getActionList().size()).isEqualTo(5); + } + + @Test + public void performMoveTopLeftAction_matchPosition() { + final boolean moveTopLeftAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_top_left, + null); + + assertThat(moveTopLeftAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top); + } + + @Test + public void performMoveTopRightAction_matchPosition() { + final boolean moveTopRightAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_top_right, null); + + assertThat(moveTopRightAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top); + } + + @Test + public void performMoveBottomLeftAction_matchPosition() { + final boolean moveBottomLeftAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_bottom_left, null); + + assertThat(moveBottomLeftAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom); + } + + @Test + public void performMoveBottomRightAction_matchPosition() { + final boolean moveBottomRightAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_bottom_right, null); + + assertThat(moveBottomRightAction).isTrue(); + assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right); + assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom); + } + + @Test + public void performMoveToEdgeAndHideAction_success() { + final boolean moveToEdgeAndHideAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_to_edge_and_hide, null); + + assertThat(moveToEdgeAndHideAction).isTrue(); + verify(mMenuAnimationController).moveToEdgeAndHide(); + } + + @Test + public void performMoveOutFromEdgeAction_success() { + final boolean moveOutEdgeAndShowAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_move_out_edge_and_show, null); + + assertThat(moveOutEdgeAndShowAction).isTrue(); + verify(mMenuAnimationController).moveOutEdgeAndShow(); + } + + @Test + public void performFocusAction_fadeIn() { + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + ACTION_ACCESSIBILITY_FOCUS, null); + + verify(mMenuAnimationController).fadeInNowIfEnabled(); + } + + @Test + public void performClearFocusAction_fadeOut() { + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); + + verify(mMenuAnimationController).fadeOutIfEnabled(); + } +}