• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.bar
18 
19 import android.animation.AnimatorTestRule
20 import android.app.Activity
21 import android.app.ActivityManager
22 import android.content.Context
23 import android.graphics.Insets
24 import android.graphics.Outline
25 import android.graphics.Rect
26 import android.os.Bundle
27 import android.view.View
28 import android.view.ViewGroup
29 import android.view.WindowManager
30 import android.widget.FrameLayout
31 import android.widget.FrameLayout.LayoutParams
32 import androidx.test.core.app.ActivityScenario
33 import androidx.test.core.app.ApplicationProvider
34 import androidx.test.ext.junit.runners.AndroidJUnit4
35 import androidx.test.filters.SmallTest
36 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
37 import com.android.internal.logging.testing.UiEventLoggerFake
38 import com.android.internal.protolog.ProtoLog
39 import com.android.wm.shell.bubbles.Bubble
40 import com.android.wm.shell.bubbles.BubbleExpandedViewManager
41 import com.android.wm.shell.bubbles.BubbleLogger
42 import com.android.wm.shell.bubbles.BubbleOverflow
43 import com.android.wm.shell.bubbles.BubblePositioner
44 import com.android.wm.shell.bubbles.BubbleTaskView
45 import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager
46 import com.android.wm.shell.bubbles.FakeBubbleFactory
47 import com.android.wm.shell.common.TestShellExecutor
48 import com.android.wm.shell.shared.bubbles.DeviceConfig
49 import com.android.wm.shell.taskview.TaskView
50 import com.android.wm.shell.taskview.TaskViewController
51 import com.android.wm.shell.taskview.TaskViewTaskController
52 import com.google.common.truth.Truth.assertThat
53 import java.util.concurrent.Semaphore
54 import java.util.concurrent.TimeUnit
55 import org.junit.After
56 import org.junit.Before
57 import org.junit.Rule
58 import org.junit.Test
59 import org.junit.runner.RunWith
60 import org.mockito.kotlin.any
61 import org.mockito.kotlin.clearInvocations
62 import org.mockito.kotlin.eq
63 import org.mockito.kotlin.mock
64 import org.mockito.kotlin.verify
65 import org.mockito.kotlin.whenever
66 
67 /** Tests for [BubbleBarAnimationHelper] */
68 @SmallTest
69 @RunWith(AndroidJUnit4::class)
70 class BubbleBarAnimationHelperTest {
71 
72     @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
73     private lateinit var activityScenario: ActivityScenario<TestActivity>
74 
75     companion object {
76         const val SCREEN_WIDTH = 2000
77         const val SCREEN_HEIGHT = 1000
78     }
79 
80     private val context = ApplicationProvider.getApplicationContext<Context>()
81 
82     private lateinit var animationHelper: BubbleBarAnimationHelper
83     private lateinit var bubblePositioner: BubblePositioner
84     private lateinit var expandedViewManager: BubbleExpandedViewManager
85     private lateinit var bubbleLogger: BubbleLogger
86     private lateinit var mainExecutor: TestShellExecutor
87     private lateinit var bgExecutor: TestShellExecutor
88     private lateinit var container: FrameLayout
89 
90     @Before
91     fun setUp() {
92         ProtoLog.REQUIRE_PROTOLOGTOOL = false
93         ProtoLog.init()
94         activityScenario = ActivityScenario.launch(TestActivity::class.java)
95         activityScenario.onActivity { activity -> container = activity.container }
96         val windowManager = context.getSystemService(WindowManager::class.java)
97         bubblePositioner = BubblePositioner(context, windowManager)
98         bubblePositioner.setShowingInBubbleBar(true)
99         val deviceConfig =
100             DeviceConfig(
101                 windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
102                 isLargeScreen = true,
103                 isSmallTablet = false,
104                 isLandscape = true,
105                 isRtl = false,
106                 insets = Insets.of(10, 20, 30, 40),
107             )
108         bubblePositioner.update(deviceConfig)
109         expandedViewManager = FakeBubbleExpandedViewManager()
110         bubbleLogger = BubbleLogger(UiEventLoggerFake())
111 
112         mainExecutor = TestShellExecutor()
113         bgExecutor = TestShellExecutor()
114 
115         animationHelper = BubbleBarAnimationHelper(context, bubblePositioner)
116     }
117 
118     @After
119     fun tearDown() {
120         bgExecutor.flushAll()
121         mainExecutor.flushAll()
122     }
123 
124     @Test
125     fun animateSwitch_bubbleToBubble_oldHiddenNewShown() {
126         val fromBubble = createBubble(key = "from").initialize(container)
127         val toBubble = createBubble(key = "to").initialize(container)
128 
129         val semaphore = Semaphore(0)
130         val after = Runnable { semaphore.release() }
131 
132         activityScenario.onActivity {
133             animationHelper.animateSwitch(fromBubble, toBubble, after)
134             animatorTestRule.advanceTimeBy(1000)
135         }
136         getInstrumentation().waitForIdleSync()
137 
138         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
139         assertThat(fromBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE)
140         assertThat(fromBubble.bubbleBarExpandedView?.alpha).isEqualTo(0f)
141         assertThat(fromBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse()
142 
143         assertThat(toBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE)
144         assertThat(toBubble.bubbleBarExpandedView?.alpha).isEqualTo(1f)
145         assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse()
146     }
147 
148     @Test
149     fun animateSwitch_bubbleToBubble_handleColorTransferred() {
150         val fromBubble = createBubble(key = "from").initialize(container)
151         fromBubble.bubbleBarExpandedView!!
152             .handleView
153             .updateHandleColor(/* isRegionDark= */ true, /* animated= */ false)
154         val toBubble = createBubble(key = "to").initialize(container)
155 
156         activityScenario.onActivity {
157             animationHelper.animateSwitch(fromBubble, toBubble, /* afterAnimation= */ null)
158             animatorTestRule.advanceTimeBy(1000)
159         }
160         getInstrumentation().waitForIdleSync()
161 
162         assertThat(toBubble.bubbleBarExpandedView!!.handleView.handleColor)
163             .isEqualTo(fromBubble.bubbleBarExpandedView!!.handleView.handleColor)
164     }
165 
166     @Test
167     fun animateSwitch_bubbleToBubble_updateTaskBounds() {
168         val fromBubble = createBubble("from").initialize(container)
169         val toBubbleTaskController = mock<TaskViewTaskController>()
170         val taskController = mock<TaskViewController>()
171         val toBubble = createBubble("to", taskController, toBubbleTaskController).initialize(
172             container)
173 
174         activityScenario.onActivity {
175             animationHelper.animateSwitch(fromBubble, toBubble) {}
176             // Start the animation, but don't finish
177             animatorTestRule.advanceTimeBy(100)
178         }
179         getInstrumentation().waitForIdleSync()
180         // Clear invocations to ensure that bounds update happens after animation ends
181         clearInvocations(taskController)
182         getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) }
183         getInstrumentation().waitForIdleSync()
184 
185         verify(taskController).setTaskBounds(eq(toBubbleTaskController), any())
186     }
187 
188     @Test
189     fun animateSwitch_bubbleToOverflow_oldHiddenNewShown() {
190         val fromBubble = createBubble(key = "from").initialize(container)
191         val overflow = createOverflow().initialize(container)
192 
193         val semaphore = Semaphore(0)
194         val after = Runnable { semaphore.release() }
195 
196         activityScenario.onActivity {
197             animationHelper.animateSwitch(fromBubble, overflow, after)
198             animatorTestRule.advanceTimeBy(1000)
199         }
200         getInstrumentation().waitForIdleSync()
201 
202         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
203         assertThat(fromBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE)
204         assertThat(fromBubble.bubbleBarExpandedView?.alpha).isEqualTo(0f)
205         assertThat(fromBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse()
206 
207         assertThat(overflow.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE)
208         assertThat(overflow.bubbleBarExpandedView?.alpha).isEqualTo(1f)
209     }
210 
211     @Test
212     fun animateSwitch_overflowToBubble_oldHiddenNewShown() {
213         val overflow = createOverflow().initialize(container)
214         val toBubble = createBubble(key = "to").initialize(container)
215 
216         val semaphore = Semaphore(0)
217         val after = Runnable { semaphore.release() }
218 
219         activityScenario.onActivity {
220             animationHelper.animateSwitch(overflow, toBubble, after)
221             animatorTestRule.advanceTimeBy(1000)
222         }
223         getInstrumentation().waitForIdleSync()
224 
225         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
226         assertThat(overflow.bubbleBarExpandedView?.visibility).isEqualTo(View.INVISIBLE)
227         assertThat(overflow.bubbleBarExpandedView?.alpha).isEqualTo(0f)
228 
229         assertThat(toBubble.bubbleBarExpandedView?.visibility).isEqualTo(View.VISIBLE)
230         assertThat(toBubble.bubbleBarExpandedView?.alpha).isEqualTo(1f)
231         assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse()
232     }
233 
234     @Test
235     fun animateToRestPosition_updateTaskBounds() {
236         val taskView = mock<TaskViewTaskController>()
237         val controller = mock<TaskViewController>()
238         val bubble = createBubble("key", controller, taskView).initialize(container)
239 
240         val semaphore = Semaphore(0)
241         val after = Runnable { semaphore.release() }
242 
243         activityScenario.onActivity {
244             animationHelper.animateExpansion(bubble, after)
245             animatorTestRule.advanceTimeBy(1000)
246         }
247         getInstrumentation().waitForIdleSync()
248         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
249 
250         getInstrumentation().runOnMainSync {
251             animationHelper.animateToRestPosition()
252             animatorTestRule.advanceTimeBy(100)
253         }
254         // Clear invocations to ensure that bounds update happens after animation ends
255         clearInvocations(controller)
256         getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) }
257         getInstrumentation().waitForIdleSync()
258 
259         verify(controller).setTaskBounds(eq(taskView), any())
260     }
261 
262     @Test
263     fun animateExpansion() {
264         val bubble = createBubble(key = "b1").initialize(container)
265         val bbev = bubble.bubbleBarExpandedView!!
266 
267         val semaphore = Semaphore(0)
268         val after = Runnable { semaphore.release() }
269 
270         activityScenario.onActivity {
271             bbev.onTaskCreated()
272             animationHelper.animateExpansion(bubble, after)
273             animatorTestRule.advanceTimeBy(1000)
274         }
275         getInstrumentation().waitForIdleSync()
276 
277         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
278         assertThat(bbev.alpha).isEqualTo(1)
279     }
280 
281     @Test
282     fun onImeTopChanged_noOverlap() {
283         val bubble = createBubble(key = "b1").initialize(container)
284         val bbev = bubble.bubbleBarExpandedView!!
285 
286         val semaphore = Semaphore(0)
287         val after = Runnable { semaphore.release() }
288 
289         activityScenario.onActivity {
290             bbev.onTaskCreated()
291             animationHelper.animateExpansion(bubble, after)
292             animatorTestRule.advanceTimeBy(1000)
293         }
294         getInstrumentation().waitForIdleSync()
295 
296         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
297 
298         activityScenario.onActivity {
299             // notify that the IME top coordinate is greater than the bottom of the expanded view.
300             // there's no overlap so it should not be clipped.
301             animationHelper.onImeTopChanged(bbev.contentBottomOnScreen * 2)
302         }
303         val outline = Outline()
304         bbev.outlineProvider.getOutline(bbev, outline)
305         assertThat(outline.mRect.bottom).isEqualTo(bbev.height)
306     }
307 
308     @Test
309     fun onImeTopChanged_overlapsWithExpandedView() {
310         val bubble = createBubble(key = "b1").initialize(container)
311         val bbev = bubble.bubbleBarExpandedView!!
312 
313         val semaphore = Semaphore(0)
314         val after = Runnable { semaphore.release() }
315 
316         activityScenario.onActivity {
317             bbev.onTaskCreated()
318             animationHelper.animateExpansion(bubble, after)
319             animatorTestRule.advanceTimeBy(1000)
320         }
321         getInstrumentation().waitForIdleSync()
322 
323         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
324 
325         activityScenario.onActivity {
326             // notify that the IME top coordinate is less than the bottom of the expanded view,
327             // meaning it overlaps with it so we should be clipping the expanded view.
328             animationHelper.onImeTopChanged(bbev.contentBottomOnScreen - 10)
329         }
330         val outline = Outline()
331         bbev.outlineProvider.getOutline(bbev, outline)
332         assertThat(outline.mRect.bottom).isEqualTo(bbev.height - 10)
333     }
334 
335     private fun createBubble(
336         key: String,
337         taskViewController: TaskViewController = mock<TaskViewController>(),
338         taskViewTaskController: TaskViewTaskController = mock<TaskViewTaskController>(),
339     ): Bubble {
340         val taskView = TaskView(context, taskViewController, taskViewTaskController)
341         val taskInfo = mock<ActivityManager.RunningTaskInfo>()
342         whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo)
343         val bubbleTaskView = BubbleTaskView(taskView, mainExecutor)
344 
345         val bubbleBarExpandedView =
346             FakeBubbleFactory.createExpandedView(
347                 context,
348                 bubblePositioner,
349                 expandedViewManager,
350                 bubbleTaskView,
351                 mainExecutor,
352                 bgExecutor,
353                 bubbleLogger,
354             )
355         val viewInfo = FakeBubbleFactory.createViewInfo(bubbleBarExpandedView)
356         return FakeBubbleFactory.createChatBubble(context, key, viewInfo)
357     }
358 
359     private fun createOverflow(): BubbleOverflow {
360         val overflow = BubbleOverflow(context, bubblePositioner)
361         overflow.initializeForBubbleBar(expandedViewManager, bubblePositioner)
362         return overflow
363     }
364 
365     private fun Bubble.initialize(container: ViewGroup): Bubble {
366         activityScenario.onActivity { container.addView(bubbleBarExpandedView) }
367         // Mark taskView's visible
368         bubbleBarExpandedView!!.onContentVisibilityChanged(true)
369         return this
370     }
371 
372     private fun BubbleOverflow.initialize(container: ViewGroup): BubbleOverflow {
373         activityScenario.onActivity { container.addView(bubbleBarExpandedView) }
374         return this
375     }
376 
377     class TestActivity : Activity() {
378         lateinit var container: FrameLayout
379         override fun onCreate(savedInstanceState: Bundle?) {
380             super.onCreate(savedInstanceState)
381             container = FrameLayout(applicationContext)
382             container.layoutParams = LayoutParams(50, 50)
383             setContentView(container)
384         }
385     }
386 }
387