<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