• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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