Add some basic tests for BubblePositioner
Test: atest BubblePositioner
Bug: 283910436
Change-Id: I932a5ebb359354a09c9bbeb73b0aeeae2c856b6c
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
new file mode 100644
index 0000000..5f72397
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.View.LAYOUT_DIRECTION_LTR;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Configuration;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests operations and the resulting state managed by {@link BubblePositioner}.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class BubblePositionerTest extends ShellTestCase {
+
+ private static final int MIN_WIDTH_FOR_TABLET = 600;
+
+ private BubblePositioner mPositioner;
+ private Configuration mConfiguration;
+
+ @Mock
+ private WindowManager mWindowManager;
+ @Mock
+ private WindowMetrics mWindowMetrics;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mConfiguration = spy(new Configuration());
+ TestableResources testableResources = mContext.getOrCreateTestableResources();
+ testableResources.overrideConfiguration(mConfiguration);
+
+ mPositioner = new BubblePositioner(mContext, mWindowManager);
+ }
+
+ @Test
+ public void testUpdate() {
+ Insets insets = Insets.of(10, 20, 5, 15);
+ Rect screenBounds = new Rect(0, 0, 1000, 1200);
+ Rect availableRect = new Rect(screenBounds);
+ availableRect.inset(insets);
+
+ new WindowManagerConfig()
+ .setInsets(insets)
+ .setScreenBounds(screenBounds)
+ .setUpConfig();
+ mPositioner.update();
+
+ assertThat(mPositioner.getAvailableRect()).isEqualTo(availableRect);
+ assertThat(mPositioner.isLandscape()).isFalse();
+ assertThat(mPositioner.isLargeScreen()).isFalse();
+ assertThat(mPositioner.getInsets()).isEqualTo(insets);
+ }
+
+ @Test
+ public void testShowBubblesVertically_phonePortrait() {
+ new WindowManagerConfig().setOrientation(ORIENTATION_PORTRAIT).setUpConfig();
+ mPositioner.update();
+
+ assertThat(mPositioner.showBubblesVertically()).isFalse();
+ }
+
+ @Test
+ public void testShowBubblesVertically_phoneLandscape() {
+ new WindowManagerConfig().setOrientation(ORIENTATION_LANDSCAPE).setUpConfig();
+ mPositioner.update();
+
+ assertThat(mPositioner.isLandscape()).isTrue();
+ assertThat(mPositioner.showBubblesVertically()).isTrue();
+ }
+
+ @Test
+ public void testShowBubblesVertically_tablet() {
+ new WindowManagerConfig().setLargeScreen().setUpConfig();
+ mPositioner.update();
+
+ assertThat(mPositioner.showBubblesVertically()).isTrue();
+ }
+
+ /** If a resting position hasn't been set, calling it will return the default position. */
+ @Test
+ public void testGetRestingPosition_returnsDefaultPosition() {
+ new WindowManagerConfig().setUpConfig();
+ mPositioner.update();
+
+ PointF restingPosition = mPositioner.getRestingPosition();
+ PointF defaultPosition = mPositioner.getDefaultStartPosition();
+
+ assertThat(restingPosition).isEqualTo(defaultPosition);
+ }
+
+ /** If a resting position has been set, it'll return that instead of the default position. */
+ @Test
+ public void testGetRestingPosition_returnsRestingPosition() {
+ new WindowManagerConfig().setUpConfig();
+ mPositioner.update();
+
+ PointF restingPosition = new PointF(100, 100);
+ mPositioner.setRestingPosition(restingPosition);
+
+ assertThat(mPositioner.getRestingPosition()).isEqualTo(restingPosition);
+ }
+
+ /** Test that the default resting position on phone is in upper left. */
+ @Test
+ public void testGetRestingPosition_bubble_onPhone() {
+ new WindowManagerConfig().setUpConfig();
+ mPositioner.update();
+
+ RectF allowableStackRegion =
+ mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+ PointF restingPosition = mPositioner.getRestingPosition();
+
+ assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
+ assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
+ }
+
+ @Test
+ public void testGetRestingPosition_bubble_onPhone_RTL() {
+ new WindowManagerConfig().setLayoutDirection(LAYOUT_DIRECTION_RTL).setUpConfig();
+ mPositioner.update();
+
+ RectF allowableStackRegion =
+ mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+ PointF restingPosition = mPositioner.getRestingPosition();
+
+ assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
+ assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
+ }
+
+ /** Test that the default resting position on tablet is middle left. */
+ @Test
+ public void testGetRestingPosition_chatBubble_onTablet() {
+ new WindowManagerConfig().setLargeScreen().setUpConfig();
+ mPositioner.update();
+
+ RectF allowableStackRegion =
+ mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+ PointF restingPosition = mPositioner.getRestingPosition();
+
+ assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
+ assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
+ }
+
+ @Test
+ public void testGetRestingPosition_chatBubble_onTablet_RTL() {
+ new WindowManagerConfig().setLargeScreen().setLayoutDirection(
+ LAYOUT_DIRECTION_RTL).setUpConfig();
+ mPositioner.update();
+
+ RectF allowableStackRegion =
+ mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+ PointF restingPosition = mPositioner.getRestingPosition();
+
+ assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
+ assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
+ }
+
+ /**
+ * Calculates the Y position bubbles should be placed based on the config. Based on
+ * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and
+ * {@link BubbleStackView.RelativeStackPosition}.
+ */
+ private float getDefaultYPosition() {
+ final float desiredY = mContext.getResources().getDimensionPixelOffset(
+ R.dimen.bubble_stack_starting_offset_y);
+ float offsetPercent = desiredY / mPositioner.getAvailableRect().height();
+ offsetPercent = Math.max(0f, Math.min(1f, offsetPercent));
+ final RectF allowableStackRegion =
+ mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+ return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent;
+ }
+
+ /**
+ * Sets up window manager to return config values based on what you need for the test.
+ * By default it sets up a portrait phone without any insets.
+ */
+ private class WindowManagerConfig {
+ private Rect mScreenBounds = new Rect(0, 0, 1000, 2000);
+ private boolean mIsLargeScreen = false;
+ private int mOrientation = ORIENTATION_PORTRAIT;
+ private int mLayoutDirection = LAYOUT_DIRECTION_LTR;
+ private Insets mInsets = Insets.of(0, 0, 0, 0);
+
+ public WindowManagerConfig setScreenBounds(Rect screenBounds) {
+ mScreenBounds = screenBounds;
+ return this;
+ }
+
+ public WindowManagerConfig setLargeScreen() {
+ mIsLargeScreen = true;
+ return this;
+ }
+
+ public WindowManagerConfig setOrientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ public WindowManagerConfig setLayoutDirection(int layoutDirection) {
+ mLayoutDirection = layoutDirection;
+ return this;
+ }
+
+ public WindowManagerConfig setInsets(Insets insets) {
+ mInsets = insets;
+ return this;
+ }
+
+ public void setUpConfig() {
+ mConfiguration.smallestScreenWidthDp = mIsLargeScreen
+ ? MIN_WIDTH_FOR_TABLET
+ : MIN_WIDTH_FOR_TABLET - 1;
+ mConfiguration.orientation = mOrientation;
+
+ when(mConfiguration.getLayoutDirection()).thenReturn(mLayoutDirection);
+ WindowInsets windowInsets = mock(WindowInsets.class);
+ when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(mInsets);
+ when(mWindowMetrics.getWindowInsets()).thenReturn(windowInsets);
+ when(mWindowMetrics.getBounds()).thenReturn(mScreenBounds);
+ when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+ }
+ }
+}