1 /* <lambda>null2 * Copyright (C) 2025 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.desktopmode 18 19 import android.animation.AnimatorTestRule 20 import android.app.ActivityManager 21 import android.app.ActivityManager.RunningTaskInfo 22 import android.graphics.Rect 23 import android.graphics.drawable.Drawable 24 import android.graphics.drawable.LayerDrawable 25 import android.platform.test.annotations.EnableFlags 26 import android.testing.AndroidTestingRunner 27 import android.testing.TestableLooper.RunWithLooper 28 import android.view.Display 29 import android.view.Display.DEFAULT_DISPLAY 30 import android.view.SurfaceControl 31 import android.view.SurfaceControlViewHost 32 import android.view.View 33 import android.widget.FrameLayout 34 import androidx.test.filters.SmallTest 35 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE 36 import com.android.wm.shell.ShellTestCase 37 import com.android.wm.shell.TestRunningTaskInfoBuilder 38 import com.android.wm.shell.TestShellExecutor 39 import com.android.wm.shell.common.DisplayController 40 import com.android.wm.shell.common.DisplayLayout 41 import com.android.wm.shell.common.SyncTransactionQueue 42 import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider 43 import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory 44 import com.android.wm.shell.windowdecor.tiling.SnapEventHandler 45 import com.google.common.truth.Truth.assertThat 46 import kotlin.test.Test 47 import org.junit.Before 48 import org.junit.Rule 49 import org.junit.runner.RunWith 50 import org.mockito.ArgumentMatchers.anyInt 51 import org.mockito.Mock 52 import org.mockito.Mockito.mock 53 import org.mockito.kotlin.any 54 import org.mockito.kotlin.anyOrNull 55 import org.mockito.kotlin.eq 56 import org.mockito.kotlin.mock 57 import org.mockito.kotlin.never 58 import org.mockito.kotlin.spy 59 import org.mockito.kotlin.verify 60 import org.mockito.kotlin.verifyNoMoreInteractions 61 import org.mockito.kotlin.whenever 62 63 /** 64 * Test class for [VisualIndicatorViewContainer] and [VisualIndicatorAnimator] 65 * 66 * Usage: atest WMShellUnitTests:VisualIndicatorViewContainerTest 67 */ 68 @SmallTest 69 @RunWithLooper 70 @RunWith(AndroidTestingRunner::class) 71 @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) 72 class VisualIndicatorViewContainerTest : ShellTestCase() { 73 74 @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) 75 76 @Mock private lateinit var view: View 77 @Mock private lateinit var displayLayout: DisplayLayout 78 @Mock private lateinit var displayController: DisplayController 79 @Mock private lateinit var taskSurface: SurfaceControl 80 @Mock private lateinit var syncQueue: SyncTransactionQueue 81 @Mock private lateinit var mockSurfaceControlViewHostFactory: SurfaceControlViewHostFactory 82 @Mock private lateinit var mockBackground: LayerDrawable 83 @Mock private lateinit var bubbleDropTargetBoundsProvider: BubbleDropTargetBoundsProvider 84 @Mock private lateinit var snapEventHandler: SnapEventHandler 85 private val taskInfo: RunningTaskInfo = createTaskInfo() 86 private val mainExecutor = TestShellExecutor() 87 private val desktopExecutor = TestShellExecutor() 88 89 @Before 90 fun setUp() { 91 whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) 92 whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> 93 (i.arguments.first() as Rect).set(DISPLAY_BOUNDS) 94 } 95 whenever(snapEventHandler.getRightSnapBoundsIfTiled(any())).thenReturn(Rect(1, 2, 3, 4)) 96 whenever(snapEventHandler.getLeftSnapBoundsIfTiled(any())).thenReturn(Rect(5, 6, 7, 8)) 97 whenever(mockSurfaceControlViewHostFactory.create(any(), any(), any())) 98 .thenReturn(mock(SurfaceControlViewHost::class.java)) 99 } 100 101 @Test 102 fun testTransitionIndicator_sameTypeReturnsEarly() { 103 val spyViewContainer = setupSpyViewContainer() 104 // Test early return on startType == endType. 105 spyViewContainer.transitionIndicator( 106 taskInfo, 107 displayController, 108 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 109 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 110 ) 111 desktopExecutor.flushAll() 112 verify(spyViewContainer) 113 .transitionIndicator( 114 eq(taskInfo), 115 eq(displayController), 116 eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), 117 eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), 118 ) 119 // Assert fadeIn, fadeOut, and animateIndicatorType were not called. 120 verifyNoMoreInteractions(spyViewContainer) 121 } 122 123 @Test 124 fun testTransitionIndicator_firstTypeNoIndicator_callsFadeIn() { 125 val spyViewContainer = setupSpyViewContainer() 126 spyViewContainer.transitionIndicator( 127 taskInfo, 128 displayController, 129 DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, 130 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 131 ) 132 desktopExecutor.flushAll() 133 verify(spyViewContainer).fadeInIndicatorInternal(any(), any(), any(), any()) 134 } 135 136 @Test 137 fun testTransitionIndicator_secondTypeNoIndicator_callsFadeOut() { 138 val spyViewContainer = setupSpyViewContainer() 139 spyViewContainer.transitionIndicator( 140 taskInfo, 141 displayController, 142 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 143 DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, 144 ) 145 desktopExecutor.flushAll() 146 verify(spyViewContainer) 147 .fadeOutIndicator( 148 any(), 149 eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), 150 anyOrNull(), 151 eq(taskInfo.displayId), 152 eq(snapEventHandler), 153 ) 154 } 155 156 @Test 157 fun testTransitionIndicator_differentTypes_callsTransitionIndicator() { 158 val spyViewContainer = setupSpyViewContainer() 159 spyViewContainer.transitionIndicator( 160 taskInfo, 161 displayController, 162 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 163 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, 164 ) 165 desktopExecutor.flushAll() 166 verify(spyViewContainer) 167 .transitionIndicator( 168 any(), 169 any(), 170 eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), 171 eq(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR), 172 ) 173 } 174 175 @Test 176 fun testFadeInBoundsCalculation() { 177 val spyIndicator = setupSpyViewContainer() 178 val animator = 179 spyIndicator.indicatorView?.let { 180 VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsIn( 181 it, 182 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 183 displayLayout, 184 bubbleDropTargetBoundsProvider, 185 taskInfo.displayId, 186 snapEventHandler, 187 ) 188 } 189 assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(15, 15, 985, 985)) 190 assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(0, 0, 1000, 1000)) 191 } 192 193 @Test 194 fun testFadeInBoundsCalculationForLeftSnap() { 195 val spyIndicator = setupSpyViewContainer() 196 val animator = 197 spyIndicator.indicatorView?.let { 198 VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsIn( 199 it, 200 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, 201 displayLayout, 202 bubbleDropTargetBoundsProvider, 203 taskInfo.displayId, 204 snapEventHandler, 205 ) 206 } 207 208 // Right bound is the same as whatever right bound snapEventHandler returned minus padding, 209 // in this case, the right bound for the left app is 7. 210 assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(0, 0, 7, 1000)) 211 } 212 213 @Test 214 fun testFadeInBoundsCalculationForRightSnap() { 215 val spyIndicator = setupSpyViewContainer() 216 val animator = 217 spyIndicator.indicatorView?.let { 218 VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsIn( 219 it, 220 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, 221 displayLayout, 222 bubbleDropTargetBoundsProvider, 223 taskInfo.displayId, 224 snapEventHandler, 225 ) 226 } 227 228 // Left bound is the same as whatever left bound snapEventHandler returned plus padding 229 // in this case, the left bound of the right app is 1. 230 assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(1, 0, 1000, 1000)) 231 } 232 233 @Test 234 fun testFadeOutBoundsCalculation() { 235 val spyIndicator = setupSpyViewContainer() 236 val animator = 237 spyIndicator.indicatorView?.let { 238 VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsOut( 239 it, 240 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 241 displayLayout, 242 bubbleDropTargetBoundsProvider, 243 taskInfo.displayId, 244 snapEventHandler, 245 ) 246 } 247 assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(0, 0, 1000, 1000)) 248 assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(15, 15, 985, 985)) 249 } 250 251 @Test 252 fun testChangeIndicatorTypeBoundsCalculation() { 253 // Test fullscreen to split-left bounds. 254 var animator = 255 VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( 256 view, 257 displayLayout, 258 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 259 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, 260 bubbleDropTargetBoundsProvider, 261 taskInfo.displayId, 262 snapEventHandler, 263 ) 264 // Test desktop to split-right bounds. 265 animator = 266 VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( 267 view, 268 displayLayout, 269 DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, 270 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, 271 bubbleDropTargetBoundsProvider, 272 taskInfo.displayId, 273 snapEventHandler, 274 ) 275 } 276 277 @Test 278 fun fadeInIndicator_callsFadeIn() { 279 val spyViewContainer = setupSpyViewContainer() 280 281 spyViewContainer.fadeInIndicator( 282 mock<DisplayLayout>(), 283 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 284 DEFAULT_DISPLAY, 285 ) 286 desktopExecutor.flushAll() 287 288 verify(spyViewContainer).fadeInIndicatorInternal(any(), any(), any(), any()) 289 } 290 291 @Test 292 fun fadeInIndicator_alreadyReleased_doesntCallFadeIn() { 293 val spyViewContainer = setupSpyViewContainer() 294 spyViewContainer.releaseVisualIndicator() 295 296 spyViewContainer.fadeInIndicator( 297 mock<DisplayLayout>(), 298 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 299 DEFAULT_DISPLAY, 300 ) 301 desktopExecutor.flushAll() 302 303 verify(spyViewContainer, never()).fadeInIndicatorInternal(any(), any(), any(), any()) 304 } 305 306 @Test 307 @EnableFlags( 308 com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, 309 com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, 310 ) 311 fun testCreateView_bubblesEnabled_indicatorIsFrameLayout() { 312 val spyViewContainer = setupSpyViewContainer() 313 assertThat(spyViewContainer.indicatorView).isInstanceOf(FrameLayout::class.java) 314 } 315 316 @Test 317 @EnableFlags( 318 com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, 319 com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, 320 ) 321 fun testFadeInOutBubbleIndicator_addAndRemoveBarIndicator() { 322 setUpBubbleBoundsProvider() 323 val spyViewContainer = setupSpyViewContainer() 324 spyViewContainer.fadeInIndicator( 325 displayLayout, 326 DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, 327 DEFAULT_DISPLAY, 328 ) 329 desktopExecutor.flushAll() 330 animatorTestRule.advanceTimeBy(200) 331 assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() 332 333 spyViewContainer.fadeOutIndicator( 334 displayLayout, 335 DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, 336 finishCallback = null, 337 DEFAULT_DISPLAY, 338 snapEventHandler, 339 ) 340 desktopExecutor.flushAll() 341 animatorTestRule.advanceTimeBy(250) 342 assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() 343 } 344 345 @Test 346 @EnableFlags( 347 com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, 348 com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, 349 ) 350 fun testTransitionIndicator_fullscreenToBubble_addBarIndicator() { 351 setUpBubbleBoundsProvider() 352 val spyViewContainer = setupSpyViewContainer() 353 354 spyViewContainer.transitionIndicator( 355 taskInfo, 356 displayController, 357 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 358 DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, 359 ) 360 desktopExecutor.flushAll() 361 animatorTestRule.advanceTimeBy(200) 362 363 assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() 364 } 365 366 @Test 367 @EnableFlags( 368 com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, 369 com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, 370 ) 371 fun testTransitionIndicator_bubbleToFullscreen_removeBarIndicator() { 372 setUpBubbleBoundsProvider() 373 val spyViewContainer = setupSpyViewContainer() 374 spyViewContainer.fadeInIndicator( 375 displayLayout, 376 DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, 377 DEFAULT_DISPLAY, 378 ) 379 desktopExecutor.flushAll() 380 animatorTestRule.advanceTimeBy(200) 381 assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() 382 383 spyViewContainer.transitionIndicator( 384 taskInfo, 385 displayController, 386 DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, 387 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, 388 ) 389 desktopExecutor.flushAll() 390 animatorTestRule.advanceTimeBy(200) 391 392 assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() 393 } 394 395 private fun setupSpyViewContainer(): VisualIndicatorViewContainer { 396 val viewContainer = 397 VisualIndicatorViewContainer( 398 desktopExecutor, 399 mainExecutor, 400 SurfaceControl.Builder(), 401 syncQueue, 402 mockSurfaceControlViewHostFactory, 403 bubbleDropTargetBoundsProvider, 404 snapEventHandler, 405 ) 406 viewContainer.createView( 407 context, 408 mock(Display::class.java), 409 displayLayout, 410 taskInfo, 411 taskSurface, 412 ) 413 desktopExecutor.flushAll() 414 viewContainer.indicatorView?.background = mockBackground 415 whenever(mockBackground.findDrawableByLayerId(anyInt())) 416 .thenReturn(mock(Drawable::class.java)) 417 return spy(viewContainer) 418 } 419 420 private fun createTaskInfo(): RunningTaskInfo { 421 val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() 422 return TestRunningTaskInfoBuilder() 423 .setDisplayId(Display.DEFAULT_DISPLAY) 424 .setTaskDescriptionBuilder(taskDescriptionBuilder) 425 .setVisible(true) 426 .build() 427 } 428 429 private fun setUpBubbleBoundsProvider() { 430 bubbleDropTargetBoundsProvider = 431 object : BubbleDropTargetBoundsProvider { 432 override fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect { 433 return BUBBLE_INDICATOR_BOUNDS 434 } 435 436 override fun getBarDropTargetBounds(onLeft: Boolean): Rect { 437 return BAR_INDICATOR_BOUNDS 438 } 439 } 440 } 441 442 companion object { 443 private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000) 444 private val BUBBLE_INDICATOR_BOUNDS = Rect(800, 200, 900, 900) 445 private val BAR_INDICATOR_BOUNDS = Rect(880, 950, 900, 960) 446 } 447 } 448