blob: 7e9f84c1ef8c5103e5ed922e849844419e540d1a [file] [log] [blame]
/*
* 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 android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.View.OVER_SCROLL_ALWAYS;
import static android.view.View.OVER_SCROLL_NEVER;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsets.Type.systemBars;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
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.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.testing.AndroidTestingRunner;
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 androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.SmallTest;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.MotionEventHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Tests for {@link AccessibilityFloatingMenuView}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
private final List<AccessibilityTarget> mTargets = new ArrayList<>(
Collections.singletonList(mock(AccessibilityTarget.class)));
private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@Mock
private WindowManager mWindowManager;
@Mock
private ViewPropertyAnimator mAnimator;
@Mock
private WindowMetrics mWindowMetrics;
private MotionEvent mInterceptMotionEvent;
private AccessibilityFloatingMenuView mMenuView;
private RecyclerView mListView = new RecyclerView(mContext);
private int mMenuWindowHeight;
private int mMenuHalfWidth;
private int mMenuHalfHeight;
private int mDisplayHalfWidth;
private int mDisplayHalfHeight;
private int mMaxWindowX;
private int mMaxWindowY;
private final int mDisplayWindowWidth = 1080;
private final int mDisplayWindowHeight = 2340;
@Before
public void initMenuView() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
mDisplayWindowHeight));
when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
mMenuView = spy(
new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
}
@Before
public void setUpMatrices() {
final Resources res = mContext.getResources();
final int margin =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
final int padding =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
final int iconWidthHeight =
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
final int menuWidth = padding * 2 + iconWidthHeight;
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
mMenuHalfWidth = menuWidth / 2;
mMenuHalfHeight = menuHeight / 2;
mDisplayHalfWidth = mDisplayWindowWidth / 2;
mDisplayHalfHeight = mDisplayWindowHeight / 2;
int marginStartEnd =
mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
? margin : 0;
mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
mMenuWindowHeight = menuHeight + margin * 2;
mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
}
@Test
public void initListView_success() {
assertThat(mListView.getCompatAccessibilityDelegate()).isNotNull();
assertThat(mMenuView.getChildCount()).isEqualTo(1);
}
@Test
public void showMenuView_success() {
mMenuView.show();
assertThat(mMenuView.isShowing()).isTrue();
verify(mWindowManager).addView(eq(mMenuView), any(WindowManager.LayoutParams.class));
}
@Test
public void showMenuView_showTwice_addViewOnce() {
mMenuView.show();
mMenuView.show();
assertThat(mMenuView.isShowing()).isTrue();
verify(mWindowManager, times(1)).addView(eq(mMenuView),
any(WindowManager.LayoutParams.class));
}
@Test
public void hideMenuView_success() {
mMenuView.show();
mMenuView.hide();
assertThat(mMenuView.isShowing()).isFalse();
verify(mWindowManager).removeView(eq(mMenuView));
}
@Test
public void hideMenuView_hideTwice_removeViewOnce() {
mMenuView.show();
mMenuView.hide();
mMenuView.hide();
assertThat(mMenuView.isShowing()).isFalse();
verify(mWindowManager, times(1)).removeView(eq(mMenuView));
}
@Test
public void onTargetsChanged_singleTarget_expectedRadii() {
final Position alignRightPosition = new Position(1.0f, 0.0f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition);
setupBasicMenuView(menuView);
menuView.onTargetsChanged(mTargets);
final View view = menuView.getChildAt(0);
final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
final float smallRadius =
getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_small_single_radius);
final float[] expectedRadii =
new float[]{smallRadius, smallRadius, 0.0f, 0.0f, 0.0f, 0.0f, smallRadius,
smallRadius};
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
}
@Test
public void setSizeType_alignRightAndLargeSize_expectedRadii() {
final RecyclerView listView = spy(new RecyclerView(mContext));
final Position alignRightPosition = new Position(1.0f, 0.0f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition, listView);
setupBasicMenuView(menuView);
menuView.setSizeType(/* largeSize */ 1);
final LayerDrawable layerDrawable =
(LayerDrawable) listView.getBackground();
final GradientDrawable gradientDrawable =
(GradientDrawable) layerDrawable.getDrawable(0);
final float largeRadius = getContext().getResources().getDimensionPixelSize(
R.dimen.accessibility_floating_menu_large_single_radius);
final float[] expectedRadii =
new float[] {largeRadius, largeRadius, 0.0f, 0.0f, 0.0f, 0.0f, largeRadius,
largeRadius};
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
}
@Test
public void setShapeType_halfCircle_translationX() {
final RecyclerView listView = spy(new RecyclerView(mContext));
final AccessibilityFloatingMenuView menuView =
new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, listView);
setupBasicMenuView(menuView);
doReturn(mAnimator).when(listView).animate();
menuView.setShapeType(/* halfOvalShape */ 1);
verify(mAnimator).translationX(anyFloat());
}
@Test
public void onTargetsChanged_fadeInOut() {
final InOrder inOrderMenuView = inOrder(mMenuView);
mMenuView.onTargetsChanged(mTargets);
inOrderMenuView.verify(mMenuView).fadeIn();
inOrderMenuView.verify(mMenuView).fadeOut();
}
@Test
public void setSizeType_fadeInOut() {
final InOrder inOrderMenuView = inOrder(mMenuView);
mMenuView.setSizeType(/* smallSize */ 0);
inOrderMenuView.verify(mMenuView).fadeIn();
inOrderMenuView.verify(mMenuView).fadeOut();
}
@Test
public void tapOnAndDragMenu_interceptUpEvent() {
final RecyclerView listView = new RecyclerView(mContext);
final TestAccessibilityFloatingMenu menuView =
new TestAccessibilityFloatingMenu(mContext, mPlaceholderPosition, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* displayCenterX */mDisplayHalfWidth
- /* offsetXToDisplayLeftHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* displayCenterX */ mDisplayHalfWidth
- /* offsetXToDisplayLeftHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP);
}
@Test
public void tapOnAndDragMenu_matchLocation() {
final float expectedX = 1.0f;
final float expectedY = 0.7f;
final Position position = new Position(expectedX, expectedY);
final RecyclerView listView = new RecyclerView(mContext);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
position, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* displayCenterX */mDisplayHalfWidth
+ /* offsetXToDisplayRightHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* displayCenterX */ mDisplayHalfWidth
+ /* offsetXToDisplayRightHalfRegion */ 10,
/* displayCenterY */ mDisplayHalfHeight);
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
menuView.mDragAnimator.end();
assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
/* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
}
@Test
public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
final Position alignRightPosition = new Position(1.0f, 0.8f);
final RecyclerView listView = new RecyclerView(mContext);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition, listView);
setupBasicMenuView(menuView);
final int currentWindowX = menuView.mCurrentLayoutParams.x;
final int currentWindowY = menuView.mCurrentLayoutParams.y;
final MotionEvent downEvent =
mMotionEventHelper.obtainMotionEvent(0, 1,
MotionEvent.ACTION_DOWN,
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
final MotionEvent moveEvent =
mMotionEventHelper.obtainMotionEvent(2, 3,
MotionEvent.ACTION_MOVE,
/* downX */(currentWindowX + mMenuHalfWidth)
+ /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
final MotionEvent upEvent =
mMotionEventHelper.obtainMotionEvent(4, 5,
MotionEvent.ACTION_UP,
/* downX */(currentWindowX + mMenuHalfWidth)
+ /* offsetXToDisplayRightSide */ mMenuHalfWidth,
/* downY */ (currentWindowY + mMenuHalfHeight));
listView.dispatchTouchEvent(downEvent);
listView.dispatchTouchEvent(moveEvent);
listView.dispatchTouchEvent(upEvent);
assertThat(menuView.mShapeType).isEqualTo(/* halfOval */ 1);
}
@Test
public void onTargetsChanged_exceedAvailableHeight_overScrollAlways() {
doReturn(true).when(mMenuView).hasExceededMaxLayoutHeight();
mMenuView.onTargetsChanged(mTargets);
assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_ALWAYS);
}
@Test
public void onTargetsChanged_notExceedAvailableHeight_overScrollNever() {
doReturn(false).when(mMenuView).hasExceededMaxLayoutHeight();
mMenuView.onTargetsChanged(mTargets);
assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_NEVER);
}
@Test
public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() {
final int offset = 200;
final Position alignRightPosition = new Position(1.0f, 0.8f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
alignRightPosition);
setupBasicMenuView(menuView);
final WindowInsets imeInset = fakeImeInsetWith(menuView, offset);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
final int expectedLayoutY = menuView.mCurrentLayoutParams.y - offset;
menuView.dispatchApplyWindowInsets(imeInset);
assertThat(menuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
}
@Test
public void hideIme_onMenuViewShifted_menuViewMovedBack() {
final int offset = 200;
setupBasicMenuView(mMenuView);
final WindowInsets imeInset = fakeImeInsetWith(mMenuView, 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_alignDisplayTopEdge() {
final int offset = 99999;
setupBasicMenuView(mMenuView);
final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
mMenuView.dispatchApplyWindowInsets(imeInset);
assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0);
}
@Test
public void testConstructor_withPosition_expectedPosition() {
final float expectedX = 1.0f;
final float expectedY = 0.7f;
final Position position = new Position(expectedX, expectedY);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
position);
setupBasicMenuView(menuView);
assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
expectedY * mMaxWindowY);
}
@After
public void tearDown() {
mInterceptMotionEvent = null;
mMotionEventHelper.recycleEvents();
mListView = null;
}
private void setupBasicMenuView(AccessibilityFloatingMenuView menuView) {
menuView.show();
menuView.onTargetsChanged(mTargets);
menuView.setSizeType(0);
menuView.setShapeType(0);
}
/**
* Based on the current menu status, fake the ime inset component {@link WindowInsets} used
* for testing.
*
* @param menuView {@link AccessibilityFloatingMenuView} that needs to be changed
* @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(AccessibilityFloatingMenuView menuView, int offset) {
// Ensure the keyboard has overlapped on the menu view.
final int fakeImeHeight =
mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
+ offset;
return new WindowInsets.Builder()
.setVisible(ime(), true)
.setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
.build();
}
private WindowInsets fakeDisplayInsets() {
final int fakeStatusBarHeight = 75;
final int fakeNavigationBarHeight = 125;
return new WindowInsets.Builder()
.setVisible(systemBars() | displayCutout(), true)
.setInsets(systemBars() | displayCutout(),
Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
.build();
}
private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
TestAccessibilityFloatingMenu(Context context, Position position, RecyclerView listView) {
super(context, position, listView);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
final boolean intercept = super.onInterceptTouchEvent(recyclerView, event);
if (intercept) {
mInterceptMotionEvent = event;
}
return intercept;
}
}
}