1 /*
2  * Copyright (C) 2022 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 package android.media.bettertogether.cts;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import static org.junit.Assert.assertThrows;
22 
23 import android.app.Instrumentation;
24 import android.content.ComponentName;
25 import android.media.browse.MediaBrowser;
26 import android.media.browse.MediaBrowser.MediaItem;
27 import android.os.Bundle;
28 import android.platform.test.annotations.AppModeNonSdkSandbox;
29 
30 import androidx.test.ext.junit.runners.AndroidJUnit4;
31 import androidx.test.platform.app.InstrumentationRegistry;
32 
33 import com.android.compatibility.common.util.FrameworkSpecificTest;
34 import com.android.compatibility.common.util.NonMainlineTest;
35 import com.android.compatibility.common.util.PollingCheck;
36 
37 import com.google.common.truth.Correspondence;
38 
39 import org.junit.After;
40 import org.junit.Ignore;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.List;
47 import java.util.concurrent.atomic.AtomicReference;
48 
49 /**
50  * Test {@link android.media.browse.MediaBrowser}.
51  */
52 @FrameworkSpecificTest
53 @NonMainlineTest
54 @RunWith(AndroidJUnit4.class)
55 @AppModeNonSdkSandbox(reason = "SDK sandbox does not need MediaBrowser.")
56 public class MediaBrowserTest {
57     // The maximum time to wait for an operation.
58     private static final long TIME_OUT_MS = 3000L;
59 
60     /**
61      * To check {@link MediaBrowser#unsubscribe} works properly,
62      * we notify to the browser after the unsubscription that the media items have changed.
63      * Then {@link MediaBrowser.SubscriptionCallback#onChildrenLoaded} should not be called.
64      *
65      * The measured time from calling {@link StubMediaBrowserService#notifyChildrenChanged}
66      * to {@link MediaBrowser.SubscriptionCallback#onChildrenLoaded} being called is about 50ms.
67      * So we make the thread sleep for 100ms to properly check that the callback is not called.
68      */
69     private static final long SLEEP_MS = 100L;
70     private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
71             "android.media.bettertogether.cts",
72             "android.media.bettertogether.cts.StubMediaBrowserService");
73     private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
74             "invalid.package", "invalid.ServiceClassName");
75 
76     private static final Correspondence<MediaBrowser.MediaItem, String>
77             MEDIA_ITEM_HAS_ID =
78             Correspondence.from((MediaBrowser.MediaItem actual, String expected) -> {
79                 if (actual == null) {
80                     return expected == null;
81                 }
82 
83                 return actual.getMediaId().equals(expected);
84             }, "has an ID of");
85 
86     private final StubConnectionCallback mConnectionCallback = new StubConnectionCallback();
87     private final StubSubscriptionCallback mSubscriptionCallback = new StubSubscriptionCallback();
88     private final StubItemCallback mItemCallback = new StubItemCallback();
89 
90     private MediaBrowser mMediaBrowser;
91 
getInstrumentation()92     private Instrumentation getInstrumentation() {
93         return InstrumentationRegistry.getInstrumentation();
94     }
95 
96     @After
tearDown()97     public void tearDown() {
98         if (mMediaBrowser != null) {
99             try {
100                 disconnectMediaBrowser();
101             } catch (Throwable t) {
102                 // Ignore.
103             }
104             mMediaBrowser = null;
105         }
106     }
107 
108     @Test
testMediaBrowser()109     public void testMediaBrowser() throws Throwable {
110         resetCallbacks();
111         createMediaBrowser(TEST_BROWSER_SERVICE);
112         runOnMainThread(() -> assertThat(mMediaBrowser.isConnected()).isFalse());
113 
114         connectMediaBrowserService();
115         runOnMainThread(() -> assertThat(mMediaBrowser.isConnected()).isTrue());
116 
117         runOnMainThread(() -> {
118             assertThat(mMediaBrowser.getServiceComponent())
119                     .isEqualTo(TEST_BROWSER_SERVICE);
120             assertThat(mMediaBrowser.getRoot())
121                     .isEqualTo(StubMediaBrowserService.MEDIA_ID_ROOT);
122             assertThat(mMediaBrowser.getExtras().getString(StubMediaBrowserService.EXTRAS_KEY))
123                     .isEqualTo(StubMediaBrowserService.EXTRAS_VALUE);
124             assertThat(mMediaBrowser.getSessionToken())
125                     .isEqualTo(StubMediaBrowserService.sSession.getSessionToken());
126         });
127 
128         disconnectMediaBrowser();
129         runOnMainThread(() -> new PollingCheck(TIME_OUT_MS) {
130             @Override
131             protected boolean check() {
132                 return !mMediaBrowser.isConnected();
133             }
134         }.run());
135     }
136 
137     @Test
testThrowingISEWhileNotConnected()138     public void testThrowingISEWhileNotConnected() throws Throwable {
139         resetCallbacks();
140         createMediaBrowser(TEST_BROWSER_SERVICE);
141         runOnMainThread(() -> assertThat(mMediaBrowser.isConnected()).isFalse());
142 
143         runOnMainThread(() -> {
144             assertThrows(IllegalStateException.class,
145                     () -> mMediaBrowser.getExtras());
146 
147             assertThrows(IllegalStateException.class,
148                     () -> mMediaBrowser.getRoot());
149 
150             assertThrows(IllegalStateException.class,
151                     () -> mMediaBrowser.getServiceComponent());
152 
153             assertThrows(IllegalStateException.class,
154                     () -> mMediaBrowser.getSessionToken());
155         });
156     }
157 
158     @Test
testConnectTwice()159     public void testConnectTwice() throws Throwable {
160         resetCallbacks();
161         createMediaBrowser(TEST_BROWSER_SERVICE);
162         connectMediaBrowserService();
163         runOnMainThread(() -> {
164             assertThrows(IllegalStateException.class,
165                     () -> mMediaBrowser.connect());
166         });
167     }
168 
169     @Test
testConnectionFailed()170     public void testConnectionFailed() throws Throwable {
171         resetCallbacks();
172         createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
173 
174         runOnMainThread(() -> mMediaBrowser.connect());
175 
176         new PollingCheck(TIME_OUT_MS) {
177             @Override
178             protected boolean check() {
179                 return mConnectionCallback.mConnectionFailedCount > 0
180                         && mConnectionCallback.mConnectedCount == 0
181                         && mConnectionCallback.mConnectionSuspendedCount == 0;
182             }
183         }.run();
184     }
185 
186     @Test
testReconnection()187     public void testReconnection() throws Throwable {
188         createMediaBrowser(TEST_BROWSER_SERVICE);
189 
190         runOnMainThread(() -> {
191             // Reconnect before the first connection was established.
192             mMediaBrowser.connect();
193             mMediaBrowser.disconnect();
194         });
195         resetCallbacks();
196         connectMediaBrowserService();
197 
198         // Test subscribe.
199         resetCallbacks();
200         runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
201                 mSubscriptionCallback));
202         new PollingCheck(TIME_OUT_MS) {
203             @Override
204             protected boolean check() {
205                 return mSubscriptionCallback.mChildrenLoadedCount > 0;
206             }
207         }.run();
208 
209         // Test getItem.
210         resetCallbacks();
211         runOnMainThread(() -> mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN[0],
212                 mItemCallback));
213         new PollingCheck(TIME_OUT_MS) {
214             @Override
215             protected boolean check() {
216                 return mItemCallback.mLastMediaItem != null;
217             }
218         }.run();
219 
220         // Reconnect after connection was established.
221         disconnectMediaBrowser();
222         resetCallbacks();
223         connectMediaBrowserService();
224 
225         // Test getItem.
226         resetCallbacks();
227         runOnMainThread(() -> mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN[0],
228                 mItemCallback));
229         new PollingCheck(TIME_OUT_MS) {
230             @Override
231             protected boolean check() {
232                 return mItemCallback.mLastMediaItem != null;
233             }
234         }.run();
235     }
236 
237     @Test
testConnectionCallbackNotCalledAfterDisconnect()238     public void testConnectionCallbackNotCalledAfterDisconnect() throws Throwable {
239         createMediaBrowser(TEST_BROWSER_SERVICE);
240         runOnMainThread(() -> {
241             mMediaBrowser.connect();
242             mMediaBrowser.disconnect();
243         });
244         resetCallbacks();
245         try {
246             Thread.sleep(SLEEP_MS);
247         } catch (InterruptedException e) {
248             assertWithMessage("Unexpected InterruptedException occurred.").fail();
249         }
250 
251         assertThat(mConnectionCallback.mConnectedCount)
252                 .isEqualTo(0);
253         assertThat(mConnectionCallback.mConnectionFailedCount)
254                 .isEqualTo(0);
255         assertThat(mConnectionCallback.mConnectionSuspendedCount)
256                 .isEqualTo(0);
257     }
258 
259     @Test
testSubscribe()260     public void testSubscribe() throws Throwable {
261         resetCallbacks();
262         createMediaBrowser(TEST_BROWSER_SERVICE);
263         connectMediaBrowserService();
264         runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
265                 mSubscriptionCallback));
266         new PollingCheck(TIME_OUT_MS) {
267             @Override
268             protected boolean check() {
269                 return mSubscriptionCallback.mChildrenLoadedCount > 0;
270             }
271         }.run();
272 
273         assertThat(mSubscriptionCallback.mLastParentId)
274                 .isEqualTo(StubMediaBrowserService.MEDIA_ID_ROOT);
275         assertThat(mSubscriptionCallback.mLastChildMediaItems)
276                 .hasSize(StubMediaBrowserService.MEDIA_ID_CHILDREN.length);
277 
278         assertThat(mSubscriptionCallback.mLastChildMediaItems)
279                 .comparingElementsUsing(MEDIA_ITEM_HAS_ID)
280                 .containsExactlyElementsIn(StubMediaBrowserService.MEDIA_ID_CHILDREN)
281                 .inOrder();
282 
283         // Test unsubscribe.
284         resetCallbacks();
285         runOnMainThread(() -> mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT));
286 
287         // After unsubscribing, make StubMediaBrowserService notify that the children are changed.
288         StubMediaBrowserService.sInstance.notifyChildrenChanged(
289                 StubMediaBrowserService.MEDIA_ID_ROOT);
290         try {
291             Thread.sleep(SLEEP_MS);
292         } catch (InterruptedException e) {
293             assertWithMessage("Unexpected InterruptedException occurred.").fail();
294         }
295         // onChildrenLoaded should not be called.
296         assertThat(mSubscriptionCallback.mChildrenLoadedCount)
297                 .isEqualTo(0);
298     }
299 
300     @Test
testSubscribeWithIllegalArguments()301     public void testSubscribeWithIllegalArguments() throws Throwable {
302         createMediaBrowser(TEST_BROWSER_SERVICE);
303 
304         runOnMainThread(() -> {
305             assertThrows(IllegalArgumentException.class, () -> {
306                 final String nullMediaId = null;
307                 mMediaBrowser.subscribe(nullMediaId, mSubscriptionCallback);
308             });
309 
310             assertThrows(IllegalArgumentException.class, () -> {
311                 final String emptyMediaId = "";
312                 mMediaBrowser.subscribe(emptyMediaId, mSubscriptionCallback);
313             });
314 
315             assertThrows(IllegalArgumentException.class, () -> {
316                 final MediaBrowser.SubscriptionCallback nullCallback = null;
317                 mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullCallback);
318             });
319 
320             assertThrows(IllegalArgumentException.class, () -> {
321                 final Bundle nullOptions = null;
322                 mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullOptions,
323                         mSubscriptionCallback);
324             });
325         });
326     }
327 
328     @Test
testSubscribeWithOptions()329     public void testSubscribeWithOptions() throws Throwable {
330         createMediaBrowser(TEST_BROWSER_SERVICE);
331         connectMediaBrowserService();
332         final int pageSize = 3;
333         final int lastPage = (StubMediaBrowserService.MEDIA_ID_CHILDREN.length - 1) / pageSize;
334         Bundle options = new Bundle();
335         options.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
336         for (int page = 0; page <= lastPage; ++page) {
337             resetCallbacks();
338             options.putInt(MediaBrowser.EXTRA_PAGE, page);
339             runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
340                     options, mSubscriptionCallback));
341             new PollingCheck(TIME_OUT_MS) {
342                 @Override
343                 protected boolean check() {
344                     return mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0;
345                 }
346             }.run();
347             assertThat(mSubscriptionCallback.mLastParentId)
348                     .isEqualTo(StubMediaBrowserService.MEDIA_ID_ROOT);
349             if (page != lastPage) {
350                 assertThat(mSubscriptionCallback.mLastChildMediaItems).hasSize(pageSize);
351             } else {
352                 assertThat(mSubscriptionCallback.mLastChildMediaItems)
353                         .hasSize((StubMediaBrowserService.MEDIA_ID_CHILDREN.length - 1)
354                                 % pageSize + 1);
355             }
356 
357             final int lastChildMediaItemsCount = mSubscriptionCallback.mLastChildMediaItems.size();
358 
359             final String[] expectedMediaIds = getMediaIdsCurrentPage(
360                     StubMediaBrowserService.MEDIA_ID_CHILDREN,
361                     page, pageSize, lastChildMediaItemsCount);
362 
363             // Check whether all the items in the current page are loaded.
364             assertThat(mSubscriptionCallback.mLastChildMediaItems)
365                     .comparingElementsUsing(MEDIA_ITEM_HAS_ID)
366                     .containsExactlyElementsIn(expectedMediaIds)
367                     .inOrder();
368         }
369 
370         // Test unsubscribe with callback argument.
371         resetCallbacks();
372         runOnMainThread(() -> mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
373                 mSubscriptionCallback));
374 
375         // After unsubscribing, make StubMediaBrowserService notify that the children are changed.
376         StubMediaBrowserService.sInstance.notifyChildrenChanged(
377                 StubMediaBrowserService.MEDIA_ID_ROOT);
378         try {
379             Thread.sleep(SLEEP_MS);
380         } catch (InterruptedException e) {
381             assertWithMessage("Unexpected InterruptedException occurred.").fail();
382         }
383         // onChildrenLoaded should not be called.
384         assertThat(mSubscriptionCallback.mChildrenLoadedCount).isEqualTo(0);
385     }
386 
387     @Test
testSubscribeInvalidItem()388     public void testSubscribeInvalidItem() throws Throwable {
389         resetCallbacks();
390         createMediaBrowser(TEST_BROWSER_SERVICE);
391         connectMediaBrowserService();
392         runOnMainThread(() -> mMediaBrowser.subscribe(
393                 StubMediaBrowserService.MEDIA_ID_INVALID, mSubscriptionCallback));
394         new PollingCheck(TIME_OUT_MS) {
395             @Override
396             protected boolean check() {
397                 return mSubscriptionCallback.mLastErrorId != null;
398             }
399         }.run();
400 
401         assertThat(mSubscriptionCallback.mLastErrorId)
402                 .isEqualTo(StubMediaBrowserService.MEDIA_ID_INVALID);
403     }
404 
405     @Test
testSubscribeInvalidItemWithOptions()406     public void testSubscribeInvalidItemWithOptions() throws Throwable {
407         resetCallbacks();
408         createMediaBrowser(TEST_BROWSER_SERVICE);
409         connectMediaBrowserService();
410 
411         final int pageSize = 5;
412         final int page = 2;
413         Bundle options = new Bundle();
414         options.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
415         options.putInt(MediaBrowser.EXTRA_PAGE, page);
416         runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_INVALID,
417                 options, mSubscriptionCallback));
418         new PollingCheck(TIME_OUT_MS) {
419             @Override
420             protected boolean check() {
421                 return mSubscriptionCallback.mLastErrorId != null;
422             }
423         }.run();
424 
425         assertThat(mSubscriptionCallback.mLastErrorId)
426                 .isEqualTo(StubMediaBrowserService.MEDIA_ID_INVALID);
427         assertThat(mSubscriptionCallback.mLastOptions.getInt(MediaBrowser.EXTRA_PAGE))
428                 .isEqualTo(page);
429         assertThat(mSubscriptionCallback.mLastOptions.getInt(MediaBrowser.EXTRA_PAGE_SIZE))
430                 .isEqualTo(pageSize);
431     }
432 
433     @Ignore // TODO(b/291800179): Diagnose flakiness and re-enable.
434     @Test
testSubscriptionCallbackNotCalledAfterDisconnect()435     public void testSubscriptionCallbackNotCalledAfterDisconnect() throws Throwable {
436         createMediaBrowser(TEST_BROWSER_SERVICE);
437         connectMediaBrowserService();
438         runOnMainThread(() -> {
439             mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, mSubscriptionCallback);
440             mMediaBrowser.disconnect();
441         });
442         resetCallbacks();
443         StubMediaBrowserService.sInstance.notifyChildrenChanged(
444                 StubMediaBrowserService.MEDIA_ID_ROOT);
445         try {
446             Thread.sleep(SLEEP_MS);
447         } catch (InterruptedException e) {
448             assertWithMessage("Unexpected InterruptedException occurred.").fail();
449         }
450         assertThat(mSubscriptionCallback.mChildrenLoadedCount).isEqualTo(0);
451         assertThat(mSubscriptionCallback.mChildrenLoadedWithOptionCount).isEqualTo(0);
452         assertThat(mSubscriptionCallback.mLastParentId).isNull();
453     }
454 
455     @Test
testUnsubscribeWithIllegalArguments()456     public void testUnsubscribeWithIllegalArguments() throws Throwable {
457         createMediaBrowser(TEST_BROWSER_SERVICE);
458         runOnMainThread(() -> {
459             assertThrows(IllegalArgumentException.class, () -> {
460                 final String nullMediaId = null;
461                 mMediaBrowser.unsubscribe(nullMediaId);
462             });
463 
464             assertThrows(IllegalArgumentException.class, () -> {
465                 final String emptyMediaId = "";
466                 mMediaBrowser.unsubscribe(emptyMediaId);
467             });
468 
469             assertThrows(IllegalArgumentException.class, () -> {
470                 final MediaBrowser.SubscriptionCallback nullCallback = null;
471                 mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT, nullCallback);
472             });
473         });
474     }
475 
476     @Test
testUnsubscribeForMultipleSubscriptions()477     public void testUnsubscribeForMultipleSubscriptions() throws Throwable {
478         createMediaBrowser(TEST_BROWSER_SERVICE);
479         connectMediaBrowserService();
480         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
481         final int pageSize = 1;
482 
483         // Subscribe four pages, one item per page.
484         for (int page = 0; page < 4; page++) {
485             final StubSubscriptionCallback callback = new StubSubscriptionCallback();
486             subscriptionCallbacks.add(callback);
487 
488             Bundle options = new Bundle();
489             options.putInt(MediaBrowser.EXTRA_PAGE, page);
490             options.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
491             runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
492                     options, callback));
493 
494             // Each onChildrenLoaded() must be called.
495             new PollingCheck(TIME_OUT_MS) {
496                 @Override
497                 protected boolean check() {
498                     return callback.mChildrenLoadedWithOptionCount == 1;
499                 }
500             }.run();
501         }
502 
503         // Reset callbacks and unsubscribe.
504         for (StubSubscriptionCallback callback : subscriptionCallbacks) {
505             callback.reset();
506         }
507         runOnMainThread(() -> mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT));
508 
509         // After unsubscribing, make StubMediaBrowserService notify that the children are changed.
510         StubMediaBrowserService.sInstance.notifyChildrenChanged(
511                 StubMediaBrowserService.MEDIA_ID_ROOT);
512         try {
513             Thread.sleep(SLEEP_MS);
514         } catch (InterruptedException e) {
515             assertWithMessage("Unexpected InterruptedException occurred.").fail();
516         }
517 
518         // onChildrenLoaded should not be called.
519         for (StubSubscriptionCallback callback : subscriptionCallbacks) {
520             assertThat(callback.mChildrenLoadedWithOptionCount).isEqualTo(0);
521         }
522     }
523 
524     @Test
testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions()525     public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Throwable {
526         createMediaBrowser(TEST_BROWSER_SERVICE);
527         connectMediaBrowserService();
528         final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
529         final int pageSize = 1;
530 
531         // Subscribe four pages, one item per page.
532         for (int page = 0; page < 4; page++) {
533             final StubSubscriptionCallback callback = new StubSubscriptionCallback();
534             subscriptionCallbacks.add(callback);
535 
536             Bundle options = new Bundle();
537             options.putInt(MediaBrowser.EXTRA_PAGE, page);
538             options.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
539             runOnMainThread(() -> mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
540                     options, callback));
541 
542             // Each onChildrenLoaded() must be called.
543             new PollingCheck(TIME_OUT_MS) {
544                 @Override
545                 protected boolean check() {
546                     return callback.mChildrenLoadedWithOptionCount == 1;
547                 }
548             }.run();
549         }
550 
551         // Unsubscribe existing subscriptions one-by-one.
552         final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
553         for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
554             // Reset callbacks
555             for (StubSubscriptionCallback callback : subscriptionCallbacks) {
556                 callback.reset();
557             }
558 
559             final int index = i;
560             runOnMainThread(() -> {
561                 // Remove one subscription
562                 mMediaBrowser.unsubscribe(StubMediaBrowserService.MEDIA_ID_ROOT,
563                         subscriptionCallbacks.get(orderOfRemovingCallbacks[index]));
564             });
565 
566             // Make StubMediaBrowserService notify that the children are changed.
567             StubMediaBrowserService.sInstance.notifyChildrenChanged(
568                     StubMediaBrowserService.MEDIA_ID_ROOT);
569             try {
570                 Thread.sleep(SLEEP_MS);
571             } catch (InterruptedException e) {
572                 assertWithMessage("Unexpected InterruptedException occurred.").fail();
573             }
574 
575             // Only the remaining subscriptionCallbacks should be called.
576             for (int j = 0; j < 4; j++) {
577                 int childrenLoadedWithOptionsCount = subscriptionCallbacks
578                         .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
579                 if (j <= i) {
580                     assertThat(childrenLoadedWithOptionsCount)
581                             .isEqualTo(0);
582                 } else {
583                     assertThat(childrenLoadedWithOptionsCount)
584                             .isEqualTo(1);
585                 }
586             }
587         }
588     }
589 
590     @Test
testGetItem()591     public void testGetItem() throws Throwable {
592         resetCallbacks();
593         createMediaBrowser(TEST_BROWSER_SERVICE);
594         connectMediaBrowserService();
595 
596         runOnMainThread(() -> mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN[0],
597                 mItemCallback));
598         new PollingCheck(TIME_OUT_MS) {
599             @Override
600             protected boolean check() {
601                 return mItemCallback.mLastMediaItem != null;
602             }
603         }.run();
604 
605         assertThat(mItemCallback.mLastMediaItem.getMediaId())
606                 .isEqualTo(StubMediaBrowserService.MEDIA_ID_CHILDREN[0]);
607     }
608 
609     @Test
testGetItemThrowsIAE()610     public void testGetItemThrowsIAE() throws Throwable {
611         resetCallbacks();
612         createMediaBrowser(TEST_BROWSER_SERVICE);
613 
614         runOnMainThread(() -> {
615             assertThrows(IllegalArgumentException.class, () -> {
616                 // Calling getItem() with empty mediaId will throw IAE.
617                 mMediaBrowser.getItem("",  mItemCallback);
618             });
619 
620             assertThrows(IllegalArgumentException.class, () -> {
621                 // Calling getItem() with null mediaId will throw IAE.
622                 mMediaBrowser.getItem(null,  mItemCallback);
623             });
624 
625             assertThrows(IllegalArgumentException.class, () -> {
626                 // Calling getItem() with null itemCallback will throw IAE.
627                 mMediaBrowser.getItem("media_id",  null);
628             });
629         });
630     }
631 
632     @Test
testGetItemWhileNotConnected()633     public void testGetItemWhileNotConnected() throws Throwable {
634         resetCallbacks();
635         createMediaBrowser(TEST_BROWSER_SERVICE);
636 
637         final String mediaId = "test_media_id";
638         runOnMainThread(() -> mMediaBrowser.getItem(mediaId, mItemCallback));
639 
640         // Calling getItem while not connected will invoke ItemCallback.onError().
641         new PollingCheck(TIME_OUT_MS) {
642             @Override
643             protected boolean check() {
644                 return mItemCallback.mLastErrorId != null;
645             }
646         }.run();
647 
648         assertThat(mItemCallback.mLastErrorId)
649                 .isEqualTo(mediaId);
650     }
651 
652     @Test
testGetItemFailure()653     public void testGetItemFailure() throws Throwable {
654         resetCallbacks();
655         createMediaBrowser(TEST_BROWSER_SERVICE);
656         connectMediaBrowserService();
657         runOnMainThread(() -> mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_INVALID,
658                 mItemCallback));
659         new PollingCheck(TIME_OUT_MS) {
660             @Override
661             protected boolean check() {
662                 return mItemCallback.mLastErrorId != null;
663             }
664         }.run();
665 
666         assertThat(mItemCallback.mLastErrorId)
667                 .isEqualTo(StubMediaBrowserService.MEDIA_ID_INVALID);
668     }
669 
670     @Test
testItemCallbackNotCalledAfterDisconnect()671     public void testItemCallbackNotCalledAfterDisconnect() throws Throwable {
672         createMediaBrowser(TEST_BROWSER_SERVICE);
673         connectMediaBrowserService();
674         runOnMainThread(() -> {
675             mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN[0], mItemCallback);
676             mMediaBrowser.disconnect();
677         });
678         resetCallbacks();
679         try {
680             Thread.sleep(SLEEP_MS);
681         } catch (InterruptedException e) {
682             assertWithMessage("Unexpected InterruptedException occurred.").fail();
683         }
684         assertThat(mItemCallback.mLastMediaItem).isNull();
685         assertThat(mItemCallback.mLastErrorId).isNull();
686     }
687 
createMediaBrowser(final ComponentName component)688     private void createMediaBrowser(final ComponentName component) throws Throwable {
689         runOnMainThread(() -> mMediaBrowser = new MediaBrowser(
690                 getInstrumentation().getTargetContext(), component, mConnectionCallback, null));
691     }
692 
connectMediaBrowserService()693     private void connectMediaBrowserService() throws Throwable {
694         runOnMainThread(() -> mMediaBrowser.connect());
695         new PollingCheck(TIME_OUT_MS) {
696             @Override
697             protected boolean check() {
698                 return mConnectionCallback.mConnectedCount > 0;
699             }
700         }.run();
701     }
702 
disconnectMediaBrowser()703     private void disconnectMediaBrowser() throws Throwable {
704         runOnMainThread(() -> mMediaBrowser.disconnect());
705     }
706 
resetCallbacks()707     private void resetCallbacks() {
708         mConnectionCallback.reset();
709         mSubscriptionCallback.reset();
710         mItemCallback.reset();
711     }
712 
runOnMainThread(Runnable runnable)713     private void runOnMainThread(Runnable runnable) throws Throwable {
714         AtomicReference<Throwable> throwableRef = new AtomicReference<>();
715 
716         getInstrumentation().runOnMainSync(() -> {
717             try {
718                 runnable.run();
719             } catch (Throwable t) {
720                 throwableRef.set(t);
721             }
722         });
723 
724         Throwable t = throwableRef.get();
725         if (t != null) {
726             throw t;
727         }
728     }
729 
getMediaIdsCurrentPage( String[] mediaIds, int pageIndex, int pageSize, int itemsCount)730     private static String[] getMediaIdsCurrentPage(
731             String[] mediaIds, int pageIndex, int pageSize, int itemsCount) {
732         final int pageOffset = pageIndex * pageSize;
733         return Arrays.copyOfRange(mediaIds, pageOffset, pageOffset + itemsCount);
734     }
735 
736     private static class StubConnectionCallback extends MediaBrowser.ConnectionCallback {
737         volatile int mConnectedCount;
738         volatile int mConnectionFailedCount;
739         volatile int mConnectionSuspendedCount;
740 
reset()741         public void reset() {
742             mConnectedCount = 0;
743             mConnectionFailedCount = 0;
744             mConnectionSuspendedCount = 0;
745         }
746 
747         @Override
onConnected()748         public void onConnected() {
749             mConnectedCount++;
750         }
751 
752         @Override
onConnectionFailed()753         public void onConnectionFailed() {
754             mConnectionFailedCount++;
755         }
756 
757         @Override
onConnectionSuspended()758         public void onConnectionSuspended() {
759             mConnectionSuspendedCount++;
760         }
761     }
762 
763     private static class StubSubscriptionCallback extends MediaBrowser.SubscriptionCallback {
764         private volatile int mChildrenLoadedCount;
765         private volatile int mChildrenLoadedWithOptionCount;
766         private volatile String mLastErrorId;
767         private volatile String mLastParentId;
768         private volatile Bundle mLastOptions;
769         private volatile List<MediaBrowser.MediaItem> mLastChildMediaItems;
770 
reset()771         public void reset() {
772             mChildrenLoadedCount = 0;
773             mChildrenLoadedWithOptionCount = 0;
774             mLastErrorId = null;
775             mLastParentId = null;
776             mLastOptions = null;
777             mLastChildMediaItems = null;
778         }
779 
780         @Override
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children)781         public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
782             mChildrenLoadedCount++;
783             mLastParentId = parentId;
784             mLastChildMediaItems = children;
785         }
786 
787         @Override
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children, Bundle options)788         public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children,
789                 Bundle options) {
790             mChildrenLoadedWithOptionCount++;
791             mLastParentId = parentId;
792             mLastOptions = options;
793             mLastChildMediaItems = children;
794         }
795 
796         @Override
onError(String id)797         public void onError(String id) {
798             mLastErrorId = id;
799         }
800 
801         @Override
onError(String id, Bundle options)802         public void onError(String id, Bundle options) {
803             mLastErrorId = id;
804             mLastOptions = options;
805         }
806     }
807 
808     private static class StubItemCallback extends MediaBrowser.ItemCallback {
809         private volatile MediaBrowser.MediaItem mLastMediaItem;
810         private volatile String mLastErrorId;
811 
reset()812         public void reset() {
813             mLastMediaItem = null;
814             mLastErrorId = null;
815         }
816 
817         @Override
onItemLoaded(MediaItem item)818         public void onItemLoaded(MediaItem item) {
819             mLastMediaItem = item;
820         }
821 
822         @Override
onError(String id)823         public void onError(String id) {
824             mLastErrorId = id;
825         }
826     }
827 }
828