1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 import static android.view.View.LAYOUT_DIRECTION_LTR;
22 import static android.view.View.LAYOUT_DIRECTION_RTL;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.mockito.ArgumentMatchers.anyInt;
27 import static org.mockito.Mockito.mock;
28 import static org.mockito.Mockito.spy;
29 import static org.mockito.Mockito.when;
30 
31 import android.content.res.Configuration;
32 import android.graphics.Insets;
33 import android.graphics.PointF;
34 import android.graphics.Rect;
35 import android.graphics.RectF;
36 import android.testing.AndroidTestingRunner;
37 import android.testing.TestableLooper;
38 import android.testing.TestableResources;
39 import android.view.WindowInsets;
40 import android.view.WindowManager;
41 import android.view.WindowMetrics;
42 
43 import androidx.test.filters.SmallTest;
44 
45 import com.android.wm.shell.R;
46 import com.android.wm.shell.ShellTestCase;
47 
48 import org.junit.Before;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 import org.mockito.Mock;
52 import org.mockito.MockitoAnnotations;
53 
54 /**
55  * Tests operations and the resulting state managed by {@link BubblePositioner}.
56  */
57 @SmallTest
58 @RunWith(AndroidTestingRunner.class)
59 @TestableLooper.RunWithLooper(setAsMainLooper = true)
60 public class BubblePositionerTest extends ShellTestCase {
61 
62     private static final int MIN_WIDTH_FOR_TABLET = 600;
63 
64     private BubblePositioner mPositioner;
65     private Configuration mConfiguration;
66 
67     @Mock
68     private WindowManager mWindowManager;
69     @Mock
70     private WindowMetrics mWindowMetrics;
71 
72     @Before
setUp()73     public void setUp() {
74         MockitoAnnotations.initMocks(this);
75 
76         mConfiguration = spy(new Configuration());
77         TestableResources testableResources = mContext.getOrCreateTestableResources();
78         testableResources.overrideConfiguration(mConfiguration);
79 
80         mPositioner = new BubblePositioner(mContext, mWindowManager);
81     }
82 
83     @Test
testUpdate()84     public void testUpdate() {
85         Insets insets = Insets.of(10, 20, 5, 15);
86         Rect screenBounds = new Rect(0, 0, 1000, 1200);
87         Rect availableRect = new Rect(screenBounds);
88         availableRect.inset(insets);
89 
90         new WindowManagerConfig()
91                 .setInsets(insets)
92                 .setScreenBounds(screenBounds)
93                 .setUpConfig();
94         mPositioner.update();
95 
96         assertThat(mPositioner.getAvailableRect()).isEqualTo(availableRect);
97         assertThat(mPositioner.isLandscape()).isFalse();
98         assertThat(mPositioner.isLargeScreen()).isFalse();
99         assertThat(mPositioner.getInsets()).isEqualTo(insets);
100     }
101 
102     @Test
testShowBubblesVertically_phonePortrait()103     public void testShowBubblesVertically_phonePortrait() {
104         new WindowManagerConfig().setOrientation(ORIENTATION_PORTRAIT).setUpConfig();
105         mPositioner.update();
106 
107         assertThat(mPositioner.showBubblesVertically()).isFalse();
108     }
109 
110     @Test
testShowBubblesVertically_phoneLandscape()111     public void testShowBubblesVertically_phoneLandscape() {
112         new WindowManagerConfig().setOrientation(ORIENTATION_LANDSCAPE).setUpConfig();
113         mPositioner.update();
114 
115         assertThat(mPositioner.isLandscape()).isTrue();
116         assertThat(mPositioner.showBubblesVertically()).isTrue();
117     }
118 
119     @Test
testShowBubblesVertically_tablet()120     public void testShowBubblesVertically_tablet() {
121         new WindowManagerConfig().setLargeScreen().setUpConfig();
122         mPositioner.update();
123 
124         assertThat(mPositioner.showBubblesVertically()).isTrue();
125     }
126 
127     /** If a resting position hasn't been set, calling it will return the default position. */
128     @Test
testGetRestingPosition_returnsDefaultPosition()129     public void testGetRestingPosition_returnsDefaultPosition() {
130         new WindowManagerConfig().setUpConfig();
131         mPositioner.update();
132 
133         PointF restingPosition = mPositioner.getRestingPosition();
134         PointF defaultPosition = mPositioner.getDefaultStartPosition();
135 
136         assertThat(restingPosition).isEqualTo(defaultPosition);
137     }
138 
139     /** If a resting position has been set, it'll return that instead of the default position. */
140     @Test
testGetRestingPosition_returnsRestingPosition()141     public void testGetRestingPosition_returnsRestingPosition() {
142         new WindowManagerConfig().setUpConfig();
143         mPositioner.update();
144 
145         PointF restingPosition = new PointF(100, 100);
146         mPositioner.setRestingPosition(restingPosition);
147 
148         assertThat(mPositioner.getRestingPosition()).isEqualTo(restingPosition);
149     }
150 
151     /** Test that the default resting position on phone is in upper left. */
152     @Test
testGetRestingPosition_bubble_onPhone()153     public void testGetRestingPosition_bubble_onPhone() {
154         new WindowManagerConfig().setUpConfig();
155         mPositioner.update();
156 
157         RectF allowableStackRegion =
158                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
159         PointF restingPosition = mPositioner.getRestingPosition();
160 
161         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
162         assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
163     }
164 
165     @Test
testGetRestingPosition_bubble_onPhone_RTL()166     public void testGetRestingPosition_bubble_onPhone_RTL() {
167         new WindowManagerConfig().setLayoutDirection(LAYOUT_DIRECTION_RTL).setUpConfig();
168         mPositioner.update();
169 
170         RectF allowableStackRegion =
171                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
172         PointF restingPosition = mPositioner.getRestingPosition();
173 
174         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
175         assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
176     }
177 
178     /** Test that the default resting position on tablet is middle left. */
179     @Test
testGetRestingPosition_chatBubble_onTablet()180     public void testGetRestingPosition_chatBubble_onTablet() {
181         new WindowManagerConfig().setLargeScreen().setUpConfig();
182         mPositioner.update();
183 
184         RectF allowableStackRegion =
185                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
186         PointF restingPosition = mPositioner.getRestingPosition();
187 
188         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left);
189         assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
190     }
191 
192     @Test
testGetRestingPosition_chatBubble_onTablet_RTL()193     public void testGetRestingPosition_chatBubble_onTablet_RTL() {
194         new WindowManagerConfig().setLargeScreen().setLayoutDirection(
195                 LAYOUT_DIRECTION_RTL).setUpConfig();
196         mPositioner.update();
197 
198         RectF allowableStackRegion =
199                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
200         PointF restingPosition = mPositioner.getRestingPosition();
201 
202         assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right);
203         assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
204     }
205 
206     /** Test that the default resting position on tablet is middle right. */
207     @Test
testGetDefaultPosition_appBubble_onTablet()208     public void testGetDefaultPosition_appBubble_onTablet() {
209         new WindowManagerConfig().setLargeScreen().setUpConfig();
210         mPositioner.update();
211 
212         RectF allowableStackRegion =
213                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
214         PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
215 
216         assertThat(startPosition.x).isEqualTo(allowableStackRegion.right);
217         assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
218     }
219 
220     @Test
testGetRestingPosition_appBubble_onTablet_RTL()221     public void testGetRestingPosition_appBubble_onTablet_RTL() {
222         new WindowManagerConfig().setLargeScreen().setLayoutDirection(
223                 LAYOUT_DIRECTION_RTL).setUpConfig();
224         mPositioner.update();
225 
226         RectF allowableStackRegion =
227                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
228         PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
229 
230         assertThat(startPosition.x).isEqualTo(allowableStackRegion.left);
231         assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
232     }
233 
234     @Test
testHasUserModifiedDefaultPosition_false()235     public void testHasUserModifiedDefaultPosition_false() {
236         new WindowManagerConfig().setLargeScreen().setLayoutDirection(
237                 LAYOUT_DIRECTION_RTL).setUpConfig();
238         mPositioner.update();
239 
240         assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
241 
242         mPositioner.setRestingPosition(mPositioner.getDefaultStartPosition());
243 
244         assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
245     }
246 
247     @Test
testHasUserModifiedDefaultPosition_true()248     public void testHasUserModifiedDefaultPosition_true() {
249         new WindowManagerConfig().setLargeScreen().setLayoutDirection(
250                 LAYOUT_DIRECTION_RTL).setUpConfig();
251         mPositioner.update();
252 
253         assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
254 
255         mPositioner.setRestingPosition(new PointF(0, 100));
256 
257         assertThat(mPositioner.hasUserModifiedDefaultPosition()).isTrue();
258     }
259 
260     /**
261      * Calculates the Y position bubbles should be placed based on the config. Based on
262      * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and
263      * {@link BubbleStackView.RelativeStackPosition}.
264      */
getDefaultYPosition()265     private float getDefaultYPosition() {
266         final boolean isTablet = mPositioner.isLargeScreen();
267 
268         // On tablet the position is centered, on phone it is an offset from the top.
269         final float desiredY = isTablet
270                 ? mPositioner.getScreenRect().height() / 2f - (mPositioner.getBubbleSize() / 2f)
271                 : mContext.getResources().getDimensionPixelOffset(
272                         R.dimen.bubble_stack_starting_offset_y);
273         // Since we're visually centering the bubbles on tablet, use total screen height rather
274         // than the available height.
275         final float height = isTablet
276                 ? mPositioner.getScreenRect().height()
277                 : mPositioner.getAvailableRect().height();
278         float offsetPercent = desiredY / height;
279         offsetPercent = Math.max(0f, Math.min(1f, offsetPercent));
280         final RectF allowableStackRegion =
281                 mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
282         return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent;
283     }
284 
285     /**
286      * Sets up window manager to return config values based on what you need for the test.
287      * By default it sets up a portrait phone without any insets.
288      */
289     private class WindowManagerConfig {
290         private Rect mScreenBounds = new Rect(0, 0, 1000, 2000);
291         private boolean mIsLargeScreen = false;
292         private int mOrientation = ORIENTATION_PORTRAIT;
293         private int mLayoutDirection = LAYOUT_DIRECTION_LTR;
294         private Insets mInsets = Insets.of(0, 0, 0, 0);
295 
setScreenBounds(Rect screenBounds)296         public WindowManagerConfig setScreenBounds(Rect screenBounds) {
297             mScreenBounds = screenBounds;
298             return this;
299         }
300 
setLargeScreen()301         public WindowManagerConfig setLargeScreen() {
302             mIsLargeScreen = true;
303             return this;
304         }
305 
setOrientation(int orientation)306         public WindowManagerConfig setOrientation(int orientation) {
307             mOrientation = orientation;
308             return this;
309         }
310 
setLayoutDirection(int layoutDirection)311         public WindowManagerConfig setLayoutDirection(int layoutDirection) {
312             mLayoutDirection = layoutDirection;
313             return this;
314         }
315 
setInsets(Insets insets)316         public WindowManagerConfig setInsets(Insets insets) {
317             mInsets = insets;
318             return this;
319         }
320 
setUpConfig()321         public void setUpConfig() {
322             mConfiguration.smallestScreenWidthDp = mIsLargeScreen
323                     ? MIN_WIDTH_FOR_TABLET
324                     : MIN_WIDTH_FOR_TABLET - 1;
325             mConfiguration.orientation = mOrientation;
326 
327             when(mConfiguration.getLayoutDirection()).thenReturn(mLayoutDirection);
328             WindowInsets windowInsets = mock(WindowInsets.class);
329             when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(mInsets);
330             when(mWindowMetrics.getWindowInsets()).thenReturn(windowInsets);
331             when(mWindowMetrics.getBounds()).thenReturn(mScreenBounds);
332             when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
333         }
334     }
335 }
336