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