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