1 /*
2  * Copyright (C) 2020 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.systemui.media.controls.domain.pipeline
18 
19 import android.media.MediaMetadata
20 import android.media.session.MediaController
21 import android.media.session.MediaSession
22 import android.media.session.PlaybackState
23 import androidx.test.ext.junit.runners.AndroidJUnit4
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.media.controls.MediaTestUtils
27 import com.android.systemui.media.controls.shared.model.MediaData
28 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
29 import com.android.systemui.media.controls.util.MediaControllerFactory
30 import com.android.systemui.media.controls.util.MediaFlags
31 import com.android.systemui.plugins.statusbar.StatusBarStateController
32 import com.android.systemui.statusbar.SysuiStatusBarStateController
33 import com.android.systemui.util.concurrency.FakeExecutor
34 import com.android.systemui.util.time.FakeSystemClock
35 import com.google.common.truth.Truth.assertThat
36 import org.junit.Before
37 import org.junit.Rule
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 import org.mockito.ArgumentCaptor
41 import org.mockito.ArgumentMatchers.anyBoolean
42 import org.mockito.ArgumentMatchers.anyString
43 import org.mockito.Captor
44 import org.mockito.Mock
45 import org.mockito.Mockito
46 import org.mockito.Mockito.clearInvocations
47 import org.mockito.Mockito.mock
48 import org.mockito.Mockito.never
49 import org.mockito.Mockito.verify
50 import org.mockito.junit.MockitoJUnit
51 import org.mockito.kotlin.any
52 import org.mockito.kotlin.capture
53 import org.mockito.kotlin.eq
54 import org.mockito.kotlin.whenever
55 
56 private const val KEY = "KEY"
57 private const val PACKAGE = "PKG"
58 private const val SESSION_KEY = "SESSION_KEY"
59 private const val SESSION_ARTIST = "SESSION_ARTIST"
60 private const val SESSION_TITLE = "SESSION_TITLE"
61 private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
62 
anyObjectnull63 private fun <T> anyObject(): T {
64     return Mockito.anyObject<T>()
65 }
66 
67 @SmallTest
68 @RunWith(AndroidJUnit4::class)
69 class MediaTimeoutListenerTest : SysuiTestCase() {
70 
71     @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
72     @Mock private lateinit var mediaController: MediaController
73     @Mock private lateinit var logger: MediaTimeoutLogger
74     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
75     private lateinit var executor: FakeExecutor
76     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
77     @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
78     @Mock private lateinit var sessionCallback: (String) -> Unit
79     @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
80     @Captor
81     private lateinit var dozingCallbackCaptor:
82         ArgumentCaptor<StatusBarStateController.StateListener>
83     @JvmField @Rule val mockito = MockitoJUnit.rule()
84     private lateinit var metadataBuilder: MediaMetadata.Builder
85     private lateinit var playbackBuilder: PlaybackState.Builder
86     private lateinit var session: MediaSession
87     private lateinit var mediaData: MediaData
88     private lateinit var resumeData: MediaData
89     private lateinit var mediaTimeoutListener: MediaTimeoutListener
90     private var clock = FakeSystemClock()
91     @Mock private lateinit var mediaFlags: MediaFlags
92     @Mock private lateinit var smartspaceData: SmartspaceMediaData
93 
94     @Before
setupnull95     fun setup() {
96         whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
97         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
98         executor = FakeExecutor(clock)
99         mediaTimeoutListener =
100             MediaTimeoutListener(
101                 mediaControllerFactory,
102                 executor,
103                 logger,
104                 statusBarStateController,
105                 clock,
106                 mediaFlags,
107             )
108         mediaTimeoutListener.timeoutCallback = timeoutCallback
109         mediaTimeoutListener.stateCallback = stateCallback
110         mediaTimeoutListener.sessionCallback = sessionCallback
111 
112         // Create a media session and notification for testing.
113         metadataBuilder =
114             MediaMetadata.Builder().apply {
115                 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
116                 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
117             }
118         playbackBuilder =
119             PlaybackState.Builder().apply {
120                 setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
121                 setActions(PlaybackState.ACTION_PLAY)
122             }
123         session =
124             MediaSession(context, SESSION_KEY).apply {
125                 setMetadata(metadataBuilder.build())
126                 setPlaybackState(playbackBuilder.build())
127             }
128         session.setActive(true)
129 
130         mediaData =
131             MediaTestUtils.emptyMediaData.copy(
132                 app = PACKAGE,
133                 packageName = PACKAGE,
134                 token = session.sessionToken
135             )
136 
137         resumeData = mediaData.copy(token = null, active = false, resumption = true)
138     }
139 
140     @Test
testOnMediaDataLoaded_registersPlaybackListenernull141     fun testOnMediaDataLoaded_registersPlaybackListener() {
142         val playingState = mock(android.media.session.PlaybackState::class.java)
143         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
144 
145         whenever(mediaController.playbackState).thenReturn(playingState)
146         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
147         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
148         verify(logger).logPlaybackState(eq(KEY), eq(playingState))
149 
150         // Ignores if same key
151         clearInvocations(mediaController)
152         mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
153         verify(mediaController, never()).registerCallback(anyObject())
154     }
155 
156     @Test
testOnMediaDataLoaded_registersTimeout_whenPausednull157     fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
158         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
159         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
160         assertThat(executor.numPending()).isEqualTo(1)
161         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
162         verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false))
163         assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
164     }
165 
166     @Test
testOnMediaDataRemoved_unregistersPlaybackListenernull167     fun testOnMediaDataRemoved_unregistersPlaybackListener() {
168         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
169         mediaTimeoutListener.onMediaDataRemoved(KEY, false)
170         verify(mediaController).unregisterCallback(anyObject())
171 
172         // Ignores duplicate requests
173         clearInvocations(mediaController)
174         mediaTimeoutListener.onMediaDataRemoved(KEY, false)
175         verify(mediaController, never()).unregisterCallback(anyObject())
176     }
177 
178     @Test
testOnMediaDataRemoved_clearsTimeoutnull179     fun testOnMediaDataRemoved_clearsTimeout() {
180         // GIVEN media that is paused
181         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
182         assertThat(executor.numPending()).isEqualTo(1)
183         // WHEN the media is removed
184         mediaTimeoutListener.onMediaDataRemoved(KEY, false)
185         // THEN the timeout runnable is cancelled
186         assertThat(executor.numPending()).isEqualTo(0)
187     }
188 
189     @Test
testOnMediaDataLoaded_migratesKeysnull190     fun testOnMediaDataLoaded_migratesKeys() {
191         val newKey = "NEWKEY"
192         // From not playing
193         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
194         clearInvocations(mediaController)
195 
196         // To playing
197         val playingState = mock(android.media.session.PlaybackState::class.java)
198         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
199         whenever(mediaController.playbackState).thenReturn(playingState)
200         mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
201         verify(mediaController).unregisterCallback(anyObject())
202         verify(mediaController).registerCallback(anyObject())
203         verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true))
204 
205         // Enqueues callback
206         assertThat(executor.numPending()).isEqualTo(1)
207     }
208 
209     @Test
testOnMediaDataLoaded_migratesKeys_noTimeoutExtensionnull210     fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
211         val newKey = "NEWKEY"
212         // From not playing
213         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
214         clearInvocations(mediaController)
215 
216         // Migrate, still not playing
217         val playingState = mock(android.media.session.PlaybackState::class.java)
218         whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
219         whenever(mediaController.playbackState).thenReturn(playingState)
220         mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
221 
222         // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
223         // is another scheduled
224         assertThat(executor.numPending()).isEqualTo(1)
225         verify(logger).logUpdateListener(eq(newKey), eq(false))
226     }
227 
228     @Test
testOnPlaybackStateChanged_schedulesTimeout_whenPausednull229     fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() {
230         // Assuming we're registered
231         testOnMediaDataLoaded_registersPlaybackListener()
232 
233         mediaCallbackCaptor.value.onPlaybackStateChanged(
234             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
235         )
236         assertThat(executor.numPending()).isEqualTo(1)
237         assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
238     }
239 
240     @Test
testOnPlaybackStateChanged_cancelsTimeout_whenResumednull241     fun testOnPlaybackStateChanged_cancelsTimeout_whenResumed() {
242         // Assuming we have a pending timeout
243         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
244 
245         mediaCallbackCaptor.value.onPlaybackStateChanged(
246             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
247         )
248         assertThat(executor.numPending()).isEqualTo(0)
249         verify(logger).logTimeoutCancelled(eq(KEY), any())
250     }
251 
252     @Test
testOnPlaybackStateChanged_reusesTimeout_whenNotPlayingnull253     fun testOnPlaybackStateChanged_reusesTimeout_whenNotPlaying() {
254         // Assuming we have a pending timeout
255         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
256 
257         mediaCallbackCaptor.value.onPlaybackStateChanged(
258             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
259         )
260         assertThat(executor.numPending()).isEqualTo(1)
261     }
262 
263     @Test
testTimeoutCallback_invokedIfTimeoutnull264     fun testTimeoutCallback_invokedIfTimeout() {
265         // Assuming we're have a pending timeout
266         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
267 
268         with(executor) {
269             advanceClockToNext()
270             runAllReady()
271         }
272         verify(timeoutCallback).invoke(eq(KEY), eq(true))
273     }
274 
275     @Test
testIsTimedOutnull276     fun testIsTimedOut() {
277         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
278         assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
279     }
280 
281     @Test
testOnSessionDestroyed_active_clearsTimeoutnull282     fun testOnSessionDestroyed_active_clearsTimeout() {
283         // GIVEN media that is paused
284         val mediaPaused = mediaData.copy(isPlaying = false)
285         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
286         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
287         assertThat(executor.numPending()).isEqualTo(1)
288 
289         // WHEN the session is destroyed
290         mediaCallbackCaptor.value.onSessionDestroyed()
291 
292         // THEN the controller is unregistered and timeout run
293         verify(mediaController).unregisterCallback(anyObject())
294         assertThat(executor.numPending()).isEqualTo(0)
295         verify(logger).logSessionDestroyed(eq(KEY))
296         verify(sessionCallback).invoke(eq(KEY))
297     }
298 
299     @Test
testSessionDestroyed_thenRestarts_resetsTimeoutnull300     fun testSessionDestroyed_thenRestarts_resetsTimeout() {
301         // Assuming we have previously destroyed the session
302         testOnSessionDestroyed_active_clearsTimeout()
303 
304         // WHEN we get an update with media playing
305         val playingState = mock(android.media.session.PlaybackState::class.java)
306         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
307         whenever(mediaController.playbackState).thenReturn(playingState)
308         val mediaPlaying = mediaData.copy(isPlaying = true)
309         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
310 
311         // THEN the timeout runnable will update the state
312         assertThat(executor.numPending()).isEqualTo(1)
313         with(executor) {
314             advanceClockToNext()
315             runAllReady()
316         }
317         verify(timeoutCallback).invoke(eq(KEY), eq(false))
318         verify(logger).logReuseListener(eq(KEY))
319     }
320 
321     @Test
testOnSessionDestroyed_resume_continuesTimeoutnull322     fun testOnSessionDestroyed_resume_continuesTimeout() {
323         // GIVEN resume media with session info
324         val resumeWithSession = resumeData.copy(token = session.sessionToken)
325         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
326         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
327         assertThat(executor.numPending()).isEqualTo(1)
328 
329         // WHEN the session is destroyed
330         mediaCallbackCaptor.value.onSessionDestroyed()
331 
332         // THEN the controller is unregistered, but the timeout is still scheduled
333         verify(mediaController).unregisterCallback(anyObject())
334         assertThat(executor.numPending()).isEqualTo(1)
335         verify(sessionCallback, never()).invoke(eq(KEY))
336     }
337 
338     @Test
testOnMediaDataLoaded_activeToResume_registersTimeoutnull339     fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
340         // WHEN a regular media is loaded
341         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
342 
343         // AND it turns into a resume control
344         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
345 
346         // THEN we register a timeout
347         assertThat(executor.numPending()).isEqualTo(1)
348         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
349         assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
350     }
351 
352     @Test
testOnMediaDataLoaded_pausedToResume_updatesTimeoutnull353     fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
354         // WHEN regular media is paused
355         val pausedState =
356             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
357         whenever(mediaController.playbackState).thenReturn(pausedState)
358         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
359         assertThat(executor.numPending()).isEqualTo(1)
360 
361         // AND it turns into a resume control
362         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
363 
364         // THEN we update the timeout length
365         assertThat(executor.numPending()).isEqualTo(1)
366         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
367         assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
368     }
369 
370     @Test
testOnMediaDataLoaded_resumption_registersTimeoutnull371     fun testOnMediaDataLoaded_resumption_registersTimeout() {
372         // WHEN a resume media is loaded
373         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
374 
375         // THEN we register a timeout
376         assertThat(executor.numPending()).isEqualTo(1)
377         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
378         assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
379     }
380 
381     @Test
testOnMediaDataLoaded_resumeToActive_updatesTimeoutnull382     fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
383         // WHEN we have a resume control
384         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
385 
386         // AND that media is resumed
387         val playingState =
388             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
389         whenever(mediaController.playbackState).thenReturn(playingState)
390         mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
391 
392         // THEN the timeout length is changed to a regular media control
393         assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
394     }
395 
396     @Test
testOnMediaDataRemoved_resume_timeoutCancellednull397     fun testOnMediaDataRemoved_resume_timeoutCancelled() {
398         // WHEN we have a resume control
399         testOnMediaDataLoaded_resumption_registersTimeout()
400         // AND the media is removed
401         mediaTimeoutListener.onMediaDataRemoved(PACKAGE, false)
402 
403         // THEN the timeout runnable is cancelled
404         assertThat(executor.numPending()).isEqualTo(0)
405     }
406 
407     @Test
testOnMediaDataLoaded_playbackActionsChanged_noCallbacknull408     fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
409         // Load media data once
410         val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
411         loadMediaDataWithPlaybackState(pausedState)
412 
413         // When media data is loaded again, with different actions
414         val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
415         loadMediaDataWithPlaybackState(playingState)
416 
417         // Then the callback is not invoked
418         verify(stateCallback, never()).invoke(eq(KEY), any())
419     }
420 
421     @Test
testOnPlaybackStateChanged_playbackActionsChanged_sendsCallbacknull422     fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
423         // Load media data once
424         val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
425         loadMediaDataWithPlaybackState(pausedState)
426 
427         // When the playback state changes, and has different actions
428         val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
429         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
430 
431         // Then the callback is invoked
432         verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
433     }
434 
435     @Test
testOnPlaybackStateChanged_differentCustomActions_sendsCallbacknull436     fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
437         val customOne =
438             PlaybackState.CustomAction.Builder(
439                     "ACTION_1",
440                     "custom action 1",
441                     android.R.drawable.ic_media_ff
442                 )
443                 .build()
444         val pausedState =
445             PlaybackState.Builder()
446                 .setActions(PlaybackState.ACTION_PAUSE)
447                 .addCustomAction(customOne)
448                 .build()
449         loadMediaDataWithPlaybackState(pausedState)
450 
451         // When the playback state actions change
452         val customTwo =
453             PlaybackState.CustomAction.Builder(
454                     "ACTION_2",
455                     "custom action 2",
456                     android.R.drawable.ic_media_rew
457                 )
458                 .build()
459         val pausedStateTwoActions =
460             PlaybackState.Builder()
461                 .setActions(PlaybackState.ACTION_PAUSE)
462                 .addCustomAction(customOne)
463                 .addCustomAction(customTwo)
464                 .build()
465         mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
466 
467         // Then the callback is invoked
468         verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
469     }
470 
471     @Test
testOnPlaybackStateChanged_sameActions_noCallbacknull472     fun testOnPlaybackStateChanged_sameActions_noCallback() {
473         val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
474         loadMediaDataWithPlaybackState(stateWithActions)
475 
476         // When the playback state updates with the same actions
477         mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
478 
479         // Then the callback is not invoked again
480         verify(stateCallback, never()).invoke(eq(KEY), any())
481     }
482 
483     @Test
testOnPlaybackStateChanged_sameCustomActions_noCallbacknull484     fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
485         val actionName = "custom action"
486         val actionIcon = android.R.drawable.ic_media_ff
487         val customOne =
488             PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
489         val stateOne =
490             PlaybackState.Builder()
491                 .setActions(PlaybackState.ACTION_PAUSE)
492                 .addCustomAction(customOne)
493                 .build()
494         loadMediaDataWithPlaybackState(stateOne)
495 
496         // When the playback state is updated, but has the same actions
497         val customTwo =
498             PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
499         val stateTwo =
500             PlaybackState.Builder()
501                 .setActions(PlaybackState.ACTION_PAUSE)
502                 .addCustomAction(customTwo)
503                 .build()
504         mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
505 
506         // Then the callback is not invoked
507         verify(stateCallback, never()).invoke(eq(KEY), any())
508     }
509 
510     @Test
testOnMediaDataLoaded_isPlayingChanged_noCallbacknull511     fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
512         // Load media data in paused state
513         val pausedState =
514             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
515         loadMediaDataWithPlaybackState(pausedState)
516 
517         // When media data is loaded again but playing
518         val playingState =
519             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
520         loadMediaDataWithPlaybackState(playingState)
521 
522         // Then the callback is not invoked
523         verify(stateCallback, never()).invoke(eq(KEY), any())
524     }
525 
526     @Test
testOnPlaybackStateChanged_isPlayingChanged_sendsCallbacknull527     fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
528         // Load media data in paused state
529         val pausedState =
530             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
531         loadMediaDataWithPlaybackState(pausedState)
532 
533         // When the playback state changes to playing
534         val playingState =
535             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
536         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
537 
538         // Then the callback is invoked
539         verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
540     }
541 
542     @Test
testOnPlaybackStateChanged_isPlayingSame_noCallbacknull543     fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
544         // Load media data in paused state
545         val pausedState =
546             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
547         loadMediaDataWithPlaybackState(pausedState)
548 
549         // When the playback state is updated, but still not playing
550         val playingState =
551             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
552         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
553 
554         // Then the callback is not invoked
555         verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
556     }
557 
558     @Test
testTimeoutCallback_dozedPastTimeout_invokedOnWakeupnull559     fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
560         // When paused media is loaded
561         testOnMediaDataLoaded_registersPlaybackListener()
562         mediaCallbackCaptor.value.onPlaybackStateChanged(
563             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
564         )
565         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
566 
567         // And we doze past the scheduled timeout
568         val time = clock.currentTimeMillis()
569         clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
570         assertThat(executor.numPending()).isEqualTo(1)
571 
572         // Then when no longer dozing, the timeout runs immediately
573         dozingCallbackCaptor.value.onDozingChanged(false)
574         verify(timeoutCallback).invoke(eq(KEY), eq(true))
575         verify(logger).logTimeout(eq(KEY))
576 
577         // and cancel any later scheduled timeout
578         verify(logger).logTimeoutCancelled(eq(KEY), any())
579         assertThat(executor.numPending()).isEqualTo(0)
580     }
581 
582     @Test
testTimeoutCallback_dozeShortTime_notInvokedOnWakeupnull583     fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() {
584         // When paused media is loaded
585         val time = clock.currentTimeMillis()
586         clock.setElapsedRealtime(time)
587         testOnMediaDataLoaded_registersPlaybackListener()
588         mediaCallbackCaptor.value.onPlaybackStateChanged(
589             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
590         )
591         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
592 
593         // And we doze, but not past the scheduled timeout
594         clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
595         assertThat(executor.numPending()).isEqualTo(1)
596 
597         // Then when no longer dozing, the timeout remains scheduled
598         dozingCallbackCaptor.value.onDozingChanged(false)
599         verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
600         assertThat(executor.numPending()).isEqualTo(1)
601     }
602 
603     @Test
testSmartspaceDataLoaded_schedulesTimeoutnull604     fun testSmartspaceDataLoaded_schedulesTimeout() {
605         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
606         val duration = 60_000
607         val createTime = 1234L
608         val expireTime = createTime + duration
609         whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
610         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
611 
612         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
613         assertThat(executor.numPending()).isEqualTo(1)
614         assertThat(executor.advanceClockToNext()).isEqualTo(duration)
615     }
616 
617     @Test
testSmartspaceMediaData_timesOut_invokesCallbacknull618     fun testSmartspaceMediaData_timesOut_invokesCallback() {
619         // Given a pending timeout
620         testSmartspaceDataLoaded_schedulesTimeout()
621 
622         executor.runAllReady()
623         verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
624     }
625 
626     @Test
testSmartspaceDataLoaded_alreadyExists_updatesTimeoutnull627     fun testSmartspaceDataLoaded_alreadyExists_updatesTimeout() {
628         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
629         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
630         val duration = 100
631         val createTime = 1234L
632         val expireTime = createTime + duration
633         whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
634         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
635 
636         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
637         assertThat(executor.numPending()).isEqualTo(1)
638 
639         val expiryLonger = expireTime + duration
640         whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
641         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
642 
643         assertThat(executor.numPending()).isEqualTo(1)
644         assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
645     }
646 
647     @Test
testSmartspaceDataRemoved_cancelTimeoutnull648     fun testSmartspaceDataRemoved_cancelTimeout() {
649         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
650 
651         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
652         assertThat(executor.numPending()).isEqualTo(1)
653 
654         mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
655         assertThat(executor.numPending()).isEqualTo(0)
656     }
657 
658     @Test
testSmartspaceData_dozedPastTimeout_invokedOnWakeupnull659     fun testSmartspaceData_dozedPastTimeout_invokedOnWakeup() {
660         // Given a pending timeout
661         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
662         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
663         val duration = 60_000
664         val createTime = 1234L
665         val expireTime = createTime + duration
666         whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
667         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
668 
669         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
670         assertThat(executor.numPending()).isEqualTo(1)
671 
672         // And we doze past the scheduled timeout
673         val time = clock.currentTimeMillis()
674         clock.setElapsedRealtime(time + duration * 2)
675         assertThat(executor.numPending()).isEqualTo(1)
676 
677         // Then when no longer dozing, the timeout runs immediately
678         dozingCallbackCaptor.value.onDozingChanged(false)
679         verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
680         verify(logger).logTimeout(eq(SMARTSPACE_KEY))
681 
682         // and cancel any later scheduled timeout
683         assertThat(executor.numPending()).isEqualTo(0)
684     }
685 
loadMediaDataWithPlaybackStatenull686     private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
687         whenever(mediaController.playbackState).thenReturn(state)
688         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
689         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
690     }
691 }
692