<lambda>null1 package com.android.systemui.animation
2
3 import android.app.ActivityManager
4 import android.app.WindowConfiguration
5 import android.content.ComponentName
6 import android.content.pm.ActivityInfo
7 import android.content.pm.ApplicationInfo
8 import android.graphics.Point
9 import android.graphics.Rect
10 import android.os.Looper
11 import android.platform.test.annotations.DisableFlags
12 import android.platform.test.annotations.EnableFlags
13 import android.testing.TestableLooper.RunWithLooper
14 import android.view.IRemoteAnimationFinishedCallback
15 import android.view.RemoteAnimationAdapter
16 import android.view.RemoteAnimationTarget
17 import android.view.SurfaceControl
18 import android.view.ViewGroup
19 import android.view.WindowManager.TRANSIT_NONE
20 import android.widget.FrameLayout
21 import android.widget.LinearLayout
22 import android.window.RemoteTransition
23 import android.window.TransitionFilter
24 import android.window.WindowAnimationState
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.filters.SmallTest
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.shared.Flags
29 import com.android.systemui.util.mockito.any
30 import com.android.wm.shell.shared.ShellTransitions
31 import com.google.common.truth.Truth.assertThat
32 import junit.framework.Assert.assertFalse
33 import junit.framework.Assert.assertNotNull
34 import junit.framework.Assert.assertNull
35 import junit.framework.Assert.assertTrue
36 import junit.framework.AssertionFailedError
37 import kotlin.concurrent.thread
38 import kotlin.test.assertEquals
39 import kotlin.time.Duration.Companion.seconds
40 import kotlinx.coroutines.launch
41 import kotlinx.coroutines.runBlocking
42 import kotlinx.coroutines.withTimeout
43 import org.junit.After
44 import org.junit.Assert.assertThrows
45 import org.junit.Before
46 import org.junit.Rule
47 import org.junit.Test
48 import org.junit.runner.RunWith
49 import org.mockito.ArgumentCaptor
50 import org.mockito.ArgumentMatchers.anyBoolean
51 import org.mockito.Mock
52 import org.mockito.Mockito.never
53 import org.mockito.Mockito.verify
54 import org.mockito.Mockito.`when`
55 import org.mockito.Spy
56 import org.mockito.junit.MockitoJUnit
57
58 @SmallTest
59 @RunWith(AndroidJUnit4::class)
60 @RunWithLooper
61 class ActivityTransitionAnimatorTest : SysuiTestCase() {
62 private val transitionContainer = LinearLayout(mContext)
63 private val mainExecutor = context.mainExecutor
64 private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
65 private val testShellTransitions = FakeShellTransitions()
66 @Mock lateinit var callback: ActivityTransitionAnimator.Callback
67 @Mock lateinit var listener: ActivityTransitionAnimator.Listener
68 @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
69 @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
70
71 private lateinit var activityTransitionAnimator: ActivityTransitionAnimator
72 @get:Rule val rule = MockitoJUnit.rule()
73
74 @Before
75 fun setup() {
76 activityTransitionAnimator =
77 ActivityTransitionAnimator(
78 mainExecutor,
79 ActivityTransitionAnimator.TransitionRegister.fromShellTransitions(
80 testShellTransitions
81 ),
82 testTransitionAnimator,
83 testTransitionAnimator,
84 disableWmTimeout = true,
85 )
86 activityTransitionAnimator.callback = callback
87 activityTransitionAnimator.addListener(listener)
88 }
89
90 @After
91 fun tearDown() {
92 activityTransitionAnimator.removeListener(listener)
93 }
94
95 private fun startIntentWithAnimation(
96 animator: ActivityTransitionAnimator = this.activityTransitionAnimator,
97 controller: ActivityTransitionAnimator.Controller? = this.controller,
98 animate: Boolean = true,
99 intentStarter: (RemoteAnimationAdapter?) -> Int,
100 ) {
101 // We start in a new thread so that we can ensure that the callbacks are called in the main
102 // thread.
103 thread {
104 animator.startIntentWithAnimation(
105 controller = controller,
106 animate = animate,
107 intentStarter = intentStarter,
108 )
109 }
110 .join()
111 }
112
113 @Test
114 fun animationAdapterIsNullIfControllerIsNull() {
115 var startedIntent = false
116 var animationAdapter: RemoteAnimationAdapter? = null
117
118 startIntentWithAnimation(controller = null) { adapter ->
119 startedIntent = true
120 animationAdapter = adapter
121
122 ActivityManager.START_SUCCESS
123 }
124
125 assertTrue(startedIntent)
126 assertNull(animationAdapter)
127 }
128
129 @Test
130 fun animatesIfActivityOpens() {
131 val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
132 var animationAdapter: RemoteAnimationAdapter? = null
133 startIntentWithAnimation { adapter ->
134 animationAdapter = adapter
135 ActivityManager.START_SUCCESS
136 }
137
138 assertNotNull(animationAdapter)
139 waitForIdleSync()
140 verify(controller).onIntentStarted(willAnimateCaptor.capture())
141 assertTrue(willAnimateCaptor.value)
142 }
143
144 @Test
145 fun doesNotAnimateIfActivityIsAlreadyOpen() {
146 val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
147 startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP }
148
149 waitForIdleSync()
150 verify(controller).onIntentStarted(willAnimateCaptor.capture())
151 assertFalse(willAnimateCaptor.value)
152 }
153
154 @Test
155 fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
156 `when`(callback.isOnKeyguard()).thenReturn(true)
157
158 val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
159 var animationAdapter: RemoteAnimationAdapter? = null
160
161 startIntentWithAnimation(activityTransitionAnimator) { adapter ->
162 animationAdapter = adapter
163 ActivityManager.START_DELIVERED_TO_TOP
164 }
165
166 waitForIdleSync()
167 verify(controller).onIntentStarted(willAnimateCaptor.capture())
168 verify(callback).hideKeyguardWithAnimation(any())
169
170 assertTrue(willAnimateCaptor.value)
171 assertNull(animationAdapter)
172 }
173
174 @Test
175 fun doesNotAnimateIfAnimateIsFalse() {
176 val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
177 startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS }
178
179 waitForIdleSync()
180 verify(controller).onIntentStarted(willAnimateCaptor.capture())
181 assertFalse(willAnimateCaptor.value)
182 }
183
184 @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
185 @Test
186 fun registersReturnIffCookieIsPresent() {
187 `when`(callback.isOnKeyguard()).thenReturn(false)
188
189 startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
190 ActivityManager.START_DELIVERED_TO_TOP
191 }
192
193 waitForIdleSync()
194 assertTrue(testShellTransitions.remotes.isEmpty())
195 assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
196
197 val controller =
198 object : DelegateTransitionAnimatorController(controller) {
199 override val transitionCookie
200 get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
201 }
202
203 startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
204 ActivityManager.START_DELIVERED_TO_TOP
205 }
206
207 waitForIdleSync()
208 assertEquals(1, testShellTransitions.remotes.size)
209 assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
210 }
211
212 @EnableFlags(
213 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
214 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
215 )
216 @Test
217 fun registersLongLivedTransition() {
218 activityTransitionAnimator.register(
219 object : DelegateTransitionAnimatorController(controller) {
220 override val transitionCookie =
221 ActivityTransitionAnimator.TransitionCookie("test_cookie_1")
222 override val component = ComponentName("com.test.package", "Test1")
223 }
224 )
225 assertEquals(2, testShellTransitions.remotes.size)
226
227 activityTransitionAnimator.register(
228 object : DelegateTransitionAnimatorController(controller) {
229 override val transitionCookie =
230 ActivityTransitionAnimator.TransitionCookie("test_cookie_2")
231 override val component = ComponentName("com.test.package", "Test2")
232 }
233 )
234 assertEquals(4, testShellTransitions.remotes.size)
235 }
236
237 @EnableFlags(
238 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
239 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
240 )
241 @Test
242 fun registersLongLivedTransitionOverridingPreviousRegistration() {
243 val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
244 activityTransitionAnimator.register(
245 object : DelegateTransitionAnimatorController(controller) {
246 override val transitionCookie = cookie
247 override val component = ComponentName("com.test.package", "Test1")
248 }
249 )
250 val transitions = testShellTransitions.remotes.values.toList()
251
252 activityTransitionAnimator.register(
253 object : DelegateTransitionAnimatorController(controller) {
254 override val transitionCookie = cookie
255 override val component = ComponentName("com.test.package", "Test2")
256 }
257 )
258 assertEquals(2, testShellTransitions.remotes.size)
259 for (transition in transitions) {
260 assertThat(testShellTransitions.remotes.values).doesNotContain(transition)
261 }
262 }
263
264 @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
265 @Test
266 fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() {
267 val controller =
268 object : DelegateTransitionAnimatorController(controller) {
269 override val transitionCookie =
270 ActivityTransitionAnimator.TransitionCookie("test_cookie")
271 override val component = ComponentName("com.test.package", "Test")
272 }
273 assertThrows(IllegalStateException::class.java) {
274 activityTransitionAnimator.register(controller)
275 }
276 }
277
278 @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
279 @Test
280 fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
281 // No TransitionCookie
282 val controllerWithoutCookie =
283 object : DelegateTransitionAnimatorController(controller) {
284 override val transitionCookie = null
285 }
286 assertThrows(IllegalStateException::class.java) {
287 activityTransitionAnimator.register(controllerWithoutCookie)
288 }
289
290 // No ComponentName
291 val controllerWithoutComponent =
292 object : DelegateTransitionAnimatorController(controller) {
293 override val transitionCookie =
294 ActivityTransitionAnimator.TransitionCookie("test_cookie")
295 override val component = null
296 }
297 assertThrows(IllegalStateException::class.java) {
298 activityTransitionAnimator.register(controllerWithoutComponent)
299 }
300
301 // No TransitionRegister
302 activityTransitionAnimator =
303 ActivityTransitionAnimator(
304 mainExecutor,
305 transitionRegister = null,
306 testTransitionAnimator,
307 testTransitionAnimator,
308 disableWmTimeout = true,
309 )
310 val validController =
311 object : DelegateTransitionAnimatorController(controller) {
312 override val transitionCookie =
313 ActivityTransitionAnimator.TransitionCookie("test_cookie")
314 override val component = ComponentName("com.test.package", "Test")
315 }
316 assertThrows(IllegalStateException::class.java) {
317 activityTransitionAnimator.register(validController)
318 }
319 }
320
321 @EnableFlags(
322 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
323 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
324 )
325 @Test
326 fun unregistersLongLivedTransition() {
327
328 val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3)
329
330 for (index in 0 until 3) {
331 cookies[index] = ActivityTransitionAnimator.TransitionCookie("test_cookie_$index")
332
333 val controller =
334 object : DelegateTransitionAnimatorController(controller) {
335 override val transitionCookie = cookies[index]
336 override val component = ComponentName("foo.bar", "Foobar")
337 }
338 activityTransitionAnimator.register(controller)
339 }
340
341 activityTransitionAnimator.unregister(cookies[0]!!)
342 assertEquals(4, testShellTransitions.remotes.size)
343
344 activityTransitionAnimator.unregister(cookies[2]!!)
345 assertEquals(2, testShellTransitions.remotes.size)
346
347 activityTransitionAnimator.unregister(cookies[1]!!)
348 assertThat(testShellTransitions.remotes).isEmpty()
349 }
350
351 @Test
352 fun doesNotStartIfAnimationIsCancelled() {
353 val runner = activityTransitionAnimator.createRunner(controller)
354 runner.onAnimationCancelled()
355 runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
356
357 waitForIdleSync()
358 verify(controller).onTransitionAnimationCancelled()
359 verify(controller, never()).onTransitionAnimationStart(anyBoolean())
360 verify(listener).onTransitionAnimationCancelled()
361 verify(listener, never()).onTransitionAnimationStart()
362 assertNull(runner.delegate)
363 }
364
365 @Test
366 fun cancelsIfNoOpeningWindowIsFound() {
367 val runner = activityTransitionAnimator.createRunner(controller)
368 runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
369
370 waitForIdleSync()
371 verify(controller).onTransitionAnimationCancelled()
372 verify(controller, never()).onTransitionAnimationStart(anyBoolean())
373 verify(listener).onTransitionAnimationCancelled()
374 verify(listener, never()).onTransitionAnimationStart()
375 assertNull(runner.delegate)
376 }
377
378 @Test
379 fun startsAnimationIfWindowIsOpening() {
380 val runner = activityTransitionAnimator.createRunner(controller)
381 runner.onAnimationStart(
382 TRANSIT_NONE,
383 arrayOf(fakeWindow()),
384 emptyArray(),
385 emptyArray(),
386 iCallback,
387 )
388 waitForIdleSync()
389 verify(listener).onTransitionAnimationStart()
390 verify(controller).onTransitionAnimationStart(anyBoolean())
391 }
392
393 @Test
394 fun creatingControllerFromNormalViewThrows() {
395 assertThrows(IllegalArgumentException::class.java) {
396 ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
397 }
398 }
399
400 @DisableFlags(
401 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
402 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
403 )
404 @Test
405 fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() {
406 assertThrows(IllegalStateException::class.java) {
407 activityTransitionAnimator.createRunner(controller, longLived = true)
408 }
409 }
410
411 @EnableFlags(
412 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
413 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
414 )
415 @Test
416 fun runnerCreatesDelegateLazily_whenPostingTimeouts() {
417 val runner = activityTransitionAnimator.createRunner(controller, longLived = true)
418 assertNull(runner.delegate)
419 runner.postTimeouts()
420 assertNotNull(runner.delegate)
421 }
422
423 @EnableFlags(
424 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
425 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
426 )
427 @Test
428 fun runnerCreatesDelegateLazily_onAnimationStart() {
429 val runner = activityTransitionAnimator.createRunner(controller, longLived = true)
430 assertNull(runner.delegate)
431
432 // The delegate is cleaned up after execution (which happens in another thread), so what we
433 // do instead is check if it becomes non-null at any point with a 1 second timeout. This
434 // will tell us that takeOverWithAnimation() triggered the lazy initialization.
435 var delegateInitialized = false
436 runBlocking {
437 val initChecker = launch {
438 withTimeout(1.seconds) {
439 while (runner.delegate == null) continue
440 delegateInitialized = true
441 }
442 }
443 runner.onAnimationStart(
444 TRANSIT_NONE,
445 arrayOf(fakeWindow()),
446 emptyArray(),
447 emptyArray(),
448 iCallback,
449 )
450 initChecker.join()
451 }
452 assertTrue(delegateInitialized)
453 }
454
455 @EnableFlags(
456 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
457 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
458 )
459 @Test
460 fun runnerCreatesDelegateLazily_onAnimationTakeover() {
461 val runner = activityTransitionAnimator.createRunner(controller, longLived = true)
462 assertNull(runner.delegate)
463
464 // The delegate is cleaned up after execution (which happens in another thread), so what we
465 // do instead is check if it becomes non-null at any point with a 1 second timeout. This
466 // will tell us that takeOverWithAnimation() triggered the lazy initialization.
467 var delegateInitialized = false
468 runBlocking {
469 val initChecker = launch {
470 withTimeout(1.seconds) {
471 while (runner.delegate == null) continue
472 delegateInitialized = true
473 }
474 }
475 runner.takeOverAnimation(
476 arrayOf(fakeWindow()),
477 arrayOf(WindowAnimationState()),
478 SurfaceControl.Transaction(),
479 iCallback,
480 )
481 initChecker.join()
482 }
483 assertTrue(delegateInitialized)
484 }
485
486 @DisableFlags(
487 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
488 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
489 )
490 @Test
491 fun animationTakeoverThrows_whenTheFlagsAreDisabled() {
492 val runner = activityTransitionAnimator.createRunner(controller, longLived = false)
493 assertThrows(IllegalStateException::class.java) {
494 runner.takeOverAnimation(
495 arrayOf(fakeWindow()),
496 emptyArray(),
497 SurfaceControl.Transaction(),
498 iCallback,
499 )
500 }
501 }
502
503 @DisableFlags(
504 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
505 Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
506 )
507 @Test
508 fun disposeRunner_delegateDereferenced() {
509 val runner = activityTransitionAnimator.createRunner(controller)
510 assertNotNull(runner.delegate)
511 runner.dispose()
512 waitForIdleSync()
513 assertNull(runner.delegate)
514 }
515
516 private fun fakeWindow(): RemoteAnimationTarget {
517 val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
518 val taskInfo = ActivityManager.RunningTaskInfo()
519 taskInfo.topActivity = ComponentName("com.android.systemui", "FakeActivity")
520 taskInfo.topActivityInfo = ActivityInfo().apply { applicationInfo = ApplicationInfo() }
521
522 return RemoteAnimationTarget(
523 0,
524 RemoteAnimationTarget.MODE_OPENING,
525 SurfaceControl(),
526 false,
527 Rect(),
528 Rect(),
529 1,
530 Point(),
531 Rect(),
532 bounds,
533 WindowConfiguration(),
534 false,
535 SurfaceControl(),
536 Rect(),
537 taskInfo,
538 false,
539 )
540 }
541 }
542
543 /**
544 * A fake implementation of [ShellTransitions] which saves filter-transition pairs locally and
545 * allows inspection.
546 */
547 private class FakeShellTransitions : ShellTransitions {
548 val remotes = mutableMapOf<TransitionFilter, RemoteTransition>()
549 val remotesForTakeover = mutableMapOf<TransitionFilter, RemoteTransition>()
550
registerRemotenull551 override fun registerRemote(filter: TransitionFilter, remoteTransition: RemoteTransition) {
552 remotes[filter] = remoteTransition
553 }
554
registerRemoteForTakeovernull555 override fun registerRemoteForTakeover(
556 filter: TransitionFilter,
557 remoteTransition: RemoteTransition,
558 ) {
559 remotesForTakeover[filter] = remoteTransition
560 }
561
unregisterRemotenull562 override fun unregisterRemote(remoteTransition: RemoteTransition) {
563 while (remotes.containsValue(remoteTransition)) {
564 remotes.values.remove(remoteTransition)
565 }
566 while (remotesForTakeover.containsValue(remoteTransition)) {
567 remotesForTakeover.values.remove(remoteTransition)
568 }
569 }
570 }
571
572 /**
573 * A simple implementation of [ActivityTransitionAnimator.Controller] which throws if it is called
574 * outside of the main thread.
575 */
576 private class TestTransitionAnimatorController(override var transitionContainer: ViewGroup) :
577 ActivityTransitionAnimator.Controller {
578 override val isLaunching: Boolean = true
579
createAnimatorStatenull580 override fun createAnimatorState() =
581 TransitionAnimator.State(
582 top = 100,
583 bottom = 200,
584 left = 300,
585 right = 400,
586 topCornerRadius = 10f,
587 bottomCornerRadius = 20f,
588 )
589
590 private fun assertOnMainThread() {
591 if (Looper.myLooper() != Looper.getMainLooper()) {
592 throw AssertionFailedError("Called outside of main thread.")
593 }
594 }
595
onIntentStartednull596 override fun onIntentStarted(willAnimate: Boolean) {
597 assertOnMainThread()
598 }
599
onTransitionAnimationStartnull600 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
601 assertOnMainThread()
602 }
603
onTransitionAnimationProgressnull604 override fun onTransitionAnimationProgress(
605 state: TransitionAnimator.State,
606 progress: Float,
607 linearProgress: Float,
608 ) {
609 assertOnMainThread()
610 }
611
onTransitionAnimationEndnull612 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
613 assertOnMainThread()
614 }
615
onTransitionAnimationCancellednull616 override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
617 assertOnMainThread()
618 }
619 }
620