1/*
2 * Copyright 2018 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
17package androidx.media;
18
19import static androidx.media.MockMediaLibraryService2.EXTRAS;
20import static androidx.media.MockMediaLibraryService2.ROOT_ID;
21
22import static junit.framework.Assert.assertEquals;
23import static junit.framework.Assert.assertFalse;
24import static junit.framework.Assert.assertNotNull;
25import static junit.framework.Assert.assertNull;
26import static junit.framework.Assert.assertTrue;
27import static junit.framework.Assert.fail;
28
29import static org.junit.Assert.assertNotEquals;
30
31import android.content.Context;
32import android.os.Build;
33import android.os.Bundle;
34import android.os.Process;
35import android.os.ResultReceiver;
36import android.support.test.filters.LargeTest;
37import android.support.test.filters.SdkSuppress;
38import android.support.test.filters.SmallTest;
39import android.support.test.runner.AndroidJUnit4;
40
41import androidx.annotation.CallSuper;
42import androidx.annotation.GuardedBy;
43import androidx.annotation.NonNull;
44import androidx.annotation.Nullable;
45import androidx.media.MediaBrowser2.BrowserCallback;
46import androidx.media.MediaController2.ControllerCallback;
47import androidx.media.MediaLibraryService2.MediaLibrarySession;
48import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
49import androidx.media.MediaSession2.CommandButton;
50import androidx.media.MediaSession2.ControllerInfo;
51
52import junit.framework.Assert;
53
54import org.junit.Ignore;
55import org.junit.Test;
56import org.junit.runner.RunWith;
57
58import java.lang.reflect.Method;
59import java.util.List;
60import java.util.concurrent.CountDownLatch;
61import java.util.concurrent.TimeUnit;
62import java.util.concurrent.atomic.AtomicReference;
63
64/**
65 * Tests {@link MediaBrowser2}.
66 * <p>
67 * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
68 * {@link MediaController2} works cleanly.
69 */
70// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
71@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
72@RunWith(AndroidJUnit4.class)
73@SmallTest
74public class MediaBrowser2Test extends MediaController2Test {
75    private static final String TAG = "MediaBrowser2Test";
76
77    @Override
78    TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
79            final @Nullable ControllerCallback callback) throws InterruptedException {
80        final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
81        sHandler.postAndSync(new Runnable() {
82            @Override
83            public void run() {
84                // Create controller on the test handler, for changing MediaBrowserCompat's Handler
85                // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
86                // and commands wouldn't be run if tests codes waits on the test handler.
87                controller.set(new TestMediaBrowser(
88                        mContext, token, new TestBrowserCallback(callback)));
89            }
90        });
91        return controller.get();
92    }
93
94    /**
95     * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method.
96     */
97    @Test
98    public void testTestBrowserCallback() {
99        prepareLooper();
100        Method[] methods = TestBrowserCallback.class.getMethods();
101        assertNotNull(methods);
102        for (int i = 0; i < methods.length; i++) {
103            // For any methods in the controller callback, TestControllerCallback should have
104            // overriden the method and call matching API in the callback proxy.
105            assertNotEquals("TestBrowserCallback should override " + methods[i]
106                            + " and call callback proxy",
107                    BrowserCallback.class, methods[i].getDeclaringClass());
108        }
109    }
110
111    @Test
112    public void testGetLibraryRoot() throws InterruptedException {
113        prepareLooper();
114        final Bundle param = new Bundle();
115        param.putString(TAG, TAG);
116
117        final CountDownLatch latch = new CountDownLatch(1);
118        final BrowserCallback callback = new BrowserCallback() {
119            @Override
120            public void onGetLibraryRootDone(MediaBrowser2 browser,
121                    Bundle rootHints, String rootMediaId, Bundle rootExtra) {
122                assertTrue(TestUtils.equals(param, rootHints));
123                assertEquals(ROOT_ID, rootMediaId);
124                // Note that TestUtils#equals() cannot be used for this because
125                // MediaBrowserServiceCompat adds extra_client_version to the rootHints.
126                assertTrue(TestUtils.contains(rootExtra, EXTRAS));
127                latch.countDown();
128            }
129        };
130
131        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
132        MediaBrowser2 browser =
133                (MediaBrowser2) createController(token, true, callback);
134        browser.getLibraryRoot(param);
135        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
136    }
137
138    @Test
139    public void testGetItem() throws InterruptedException {
140        prepareLooper();
141        final String mediaId = MockMediaLibraryService2.MEDIA_ID_GET_ITEM;
142
143        final CountDownLatch latch = new CountDownLatch(1);
144        final BrowserCallback callback = new BrowserCallback() {
145            @Override
146            public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
147                assertEquals(mediaId, mediaIdOut);
148                assertNotNull(result);
149                assertEquals(mediaId, result.getMediaId());
150                latch.countDown();
151            }
152        };
153
154        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
155        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
156        browser.getItem(mediaId);
157        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
158    }
159
160    @Test
161    public void testGetItemNullResult() throws InterruptedException {
162        prepareLooper();
163        final String mediaId = "random_media_id";
164
165        final CountDownLatch latch = new CountDownLatch(1);
166        final BrowserCallback callback = new BrowserCallback() {
167            @Override
168            public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
169                assertEquals(mediaId, mediaIdOut);
170                assertNull(result);
171                latch.countDown();
172            }
173        };
174
175        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
176        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
177        browser.getItem(mediaId);
178        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
179    }
180
181    @Test
182    public void testGetChildren() throws InterruptedException {
183        prepareLooper();
184        final String parentId = MockMediaLibraryService2.PARENT_ID;
185        final int page = 4;
186        final int pageSize = 10;
187        final Bundle extras = new Bundle();
188        extras.putString(TAG, TAG);
189
190        final CountDownLatch latch = new CountDownLatch(1);
191        final BrowserCallback callback = new BrowserCallback() {
192            @Override
193            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut, int pageOut,
194                    int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
195                assertEquals(parentId, parentIdOut);
196                assertEquals(page, pageOut);
197                assertEquals(pageSize, pageSizeOut);
198                assertTrue(TestUtils.equals(extras, extrasOut));
199                assertNotNull(result);
200
201                int fromIndex = (page - 1) * pageSize;
202                int toIndex = Math.min(page * pageSize, MockMediaLibraryService2.CHILDREN_COUNT);
203
204                // Compare the given results with originals.
205                for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
206                    int relativeIndex = originalIndex - fromIndex;
207                    Assert.assertEquals(
208                            MockMediaLibraryService2.GET_CHILDREN_RESULT.get(originalIndex)
209                                    .getMediaId(),
210                            result.get(relativeIndex).getMediaId());
211                }
212                latch.countDown();
213            }
214        };
215
216        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
217        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
218        browser.getChildren(parentId, page, pageSize, extras);
219        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
220    }
221
222    @Test
223    public void testGetChildrenEmptyResult() throws InterruptedException {
224        prepareLooper();
225        final String parentId = MockMediaLibraryService2.PARENT_ID_NO_CHILDREN;
226
227        final CountDownLatch latch = new CountDownLatch(1);
228        final BrowserCallback callback = new BrowserCallback() {
229            @Override
230            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
231                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
232                assertNotNull(result);
233                assertEquals(0, result.size());
234                latch.countDown();
235            }
236        };
237
238        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
239        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
240        browser.getChildren(parentId, 1, 1, null);
241        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
242    }
243
244    @Test
245    public void testGetChildrenNullResult() throws InterruptedException {
246        prepareLooper();
247        final String parentId = MockMediaLibraryService2.PARENT_ID_ERROR;
248
249        final CountDownLatch latch = new CountDownLatch(1);
250        final BrowserCallback callback = new BrowserCallback() {
251            @Override
252            public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
253                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
254                assertNull(result);
255                latch.countDown();
256            }
257        };
258
259        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
260        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
261        browser.getChildren(parentId, 1, 1, null);
262        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
263    }
264
265    @Test
266    public void testSearch() throws InterruptedException {
267        prepareLooper();
268        final String query = MockMediaLibraryService2.SEARCH_QUERY;
269        final int page = 4;
270        final int pageSize = 10;
271        final Bundle extras = new Bundle();
272        extras.putString(TAG, TAG);
273
274        final CountDownLatch latchForSearch = new CountDownLatch(1);
275        final CountDownLatch latchForGetSearchResult = new CountDownLatch(1);
276        final BrowserCallback callback = new BrowserCallback() {
277            @Override
278            public void onSearchResultChanged(MediaBrowser2 browser,
279                    String queryOut, int itemCount, Bundle extrasOut) {
280                assertEquals(query, queryOut);
281                assertTrue(TestUtils.equals(extras, extrasOut));
282                assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
283                latchForSearch.countDown();
284            }
285
286            @Override
287            public void onGetSearchResultDone(MediaBrowser2 browser, String queryOut,
288                    int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
289                assertEquals(query, queryOut);
290                assertEquals(page, pageOut);
291                assertEquals(pageSize, pageSizeOut);
292                assertTrue(TestUtils.equals(extras, extrasOut));
293                assertNotNull(result);
294
295                int fromIndex = (page - 1) * pageSize;
296                int toIndex = Math.min(
297                        page * pageSize, MockMediaLibraryService2.SEARCH_RESULT_COUNT);
298
299                // Compare the given results with originals.
300                for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
301                    int relativeIndex = originalIndex - fromIndex;
302                    Assert.assertEquals(
303                            MockMediaLibraryService2.SEARCH_RESULT.get(originalIndex).getMediaId(),
304                            result.get(relativeIndex).getMediaId());
305                }
306                latchForGetSearchResult.countDown();
307            }
308        };
309
310        // Request the search.
311        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
312        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
313        browser.search(query, extras);
314        assertTrue(latchForSearch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
315
316        // Get the search result.
317        browser.getSearchResult(query, page, pageSize, extras);
318        assertTrue(latchForGetSearchResult.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
319    }
320
321    @Test
322    @LargeTest
323    public void testSearchTakesTime() throws InterruptedException {
324        prepareLooper();
325        final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
326        final Bundle extras = new Bundle();
327        extras.putString(TAG, TAG);
328
329        final CountDownLatch latch = new CountDownLatch(1);
330        final BrowserCallback callback = new BrowserCallback() {
331            @Override
332            public void onSearchResultChanged(
333                    MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
334                assertEquals(query, queryOut);
335                assertTrue(TestUtils.equals(extras, extrasOut));
336                assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
337                latch.countDown();
338            }
339        };
340
341        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
342        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
343        browser.search(query, extras);
344        assertTrue(latch.await(
345                MockMediaLibraryService2.SEARCH_TIME_IN_MS + WAIT_TIME_MS, TimeUnit.MILLISECONDS));
346    }
347
348    @Test
349    public void testSearchEmptyResult() throws InterruptedException {
350        prepareLooper();
351        final String query = MockMediaLibraryService2.SEARCH_QUERY_EMPTY_RESULT;
352        final Bundle extras = new Bundle();
353        extras.putString(TAG, TAG);
354
355        final CountDownLatch latch = new CountDownLatch(1);
356        final BrowserCallback callback = new BrowserCallback() {
357            @Override
358            public void onSearchResultChanged(
359                    MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
360                assertEquals(query, queryOut);
361                assertTrue(TestUtils.equals(extras, extrasOut));
362                assertEquals(0, itemCount);
363                latch.countDown();
364            }
365        };
366
367        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
368        MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
369        browser.search(query, extras);
370        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
371    }
372
373    @Test
374    public void testSubscribe() throws InterruptedException {
375        prepareLooper();
376        final String testParentId = "testSubscribeId";
377        final Bundle testExtras = new Bundle();
378        testExtras.putString(testParentId, testParentId);
379
380        final CountDownLatch latch = new CountDownLatch(1);
381        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
382            @Override
383            public void onSubscribe(@NonNull MediaLibraryService2.MediaLibrarySession session,
384                    @NonNull MediaSession2.ControllerInfo info, @NonNull String parentId,
385                    @Nullable Bundle extras) {
386                if (Process.myUid() == info.getUid()) {
387                    assertEquals(testParentId, parentId);
388                    assertTrue(TestUtils.equals(testExtras, extras));
389                    latch.countDown();
390                }
391            }
392        };
393        TestServiceRegistry.getInstance().setSessionCallback(callback);
394        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
395        MediaBrowser2 browser = (MediaBrowser2) createController(token);
396        browser.subscribe(testParentId, testExtras);
397        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
398    }
399
400    @Ignore
401    @Test
402    public void testUnsubscribe() throws InterruptedException {
403        prepareLooper();
404        final String testParentId = "testUnsubscribeId";
405        final CountDownLatch latch = new CountDownLatch(1);
406        final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
407            @Override
408            public void onUnsubscribe(@NonNull MediaLibrarySession session,
409                    @NonNull ControllerInfo info, @NonNull String parentId) {
410                if (Process.myUid() == info.getUid()) {
411                    assertEquals(testParentId, parentId);
412                    latch.countDown();
413                }
414            }
415        };
416        TestServiceRegistry.getInstance().setSessionCallback(callback);
417        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
418        MediaBrowser2 browser = (MediaBrowser2) createController(token);
419        browser.unsubscribe(testParentId);
420        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
421    }
422
423    @Test
424    public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed()
425            throws InterruptedException {
426        // This test uses MediaLibrarySession.notifyChildrenChanged().
427        prepareLooper();
428        final String subscribedMediaId = "subscribedMediaId";
429        final String anotherMediaId = "anotherMediaId";
430        final int testChildrenCount = 101;
431        final CountDownLatch latch = new CountDownLatch(1);
432
433        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
434            @Override
435            public void onSubscribe(@NonNull MediaLibrarySession session,
436                    @NonNull ControllerInfo controller, @NonNull String parentId,
437                    @Nullable Bundle extras) {
438                if (Process.myUid() == controller.getUid()) {
439                    // Shouldn't trigger onChildrenChanged() for the browser,
440                    // because the browser didn't subscribe this media id.
441                    session.notifyChildrenChanged(anotherMediaId, testChildrenCount, null);
442                }
443            }
444
445            @Override
446            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
447                    ControllerInfo controller, String parentId, int page, int pageSize,
448                    Bundle extras) {
449                return TestUtils.createPlaylist(testChildrenCount);
450            }
451        };
452        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
453            @Override
454            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
455                    int itemCount, Bundle extras) {
456                // Unexpected call.
457                fail();
458            }
459        };
460
461        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
462        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
463        final MediaBrowser2 browser = (MediaBrowser2) createController(
464                token, true, controllerCallbackProxy);
465        browser.subscribe(subscribedMediaId, null);
466
467        // onChildrenChanged() should not be called.
468        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
469    }
470
471    @Test
472    public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed()
473            throws InterruptedException {
474        // This test uses MediaLibrarySession.notifyChildrenChanged().
475        prepareLooper();
476        final String expectedParentId = "expectedParentId";
477        final int testChildrenCount = 101;
478        final Bundle testExtras = TestUtils.createTestBundle();
479
480        final CountDownLatch latch = new CountDownLatch(1);
481        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
482            @Override
483            public void onSubscribe(@NonNull MediaLibrarySession session,
484                    @NonNull ControllerInfo controller, @NonNull String parentId,
485                    @Nullable Bundle extras) {
486                if (Process.myUid() == controller.getUid()) {
487                    // Should trigger onChildrenChanged() for the browser.
488                    session.notifyChildrenChanged(expectedParentId, testChildrenCount, testExtras);
489                }
490            }
491
492            @Override
493            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
494                    ControllerInfo controller, String parentId, int page, int pageSize,
495                    Bundle extras) {
496                return TestUtils.createPlaylist(testChildrenCount);
497            }
498        };
499        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
500            @Override
501            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
502                    int itemCount, Bundle extras) {
503                assertEquals(expectedParentId, parentId);
504                assertEquals(testChildrenCount, itemCount);
505                assertTrue(TestUtils.equals(testExtras, extras));
506                latch.countDown();
507            }
508        };
509
510        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
511        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
512        final MediaBrowser2 browser = (MediaBrowser2) createController(
513                token, true, controllerCallbackProxy);
514        browser.subscribe(expectedParentId, null);
515
516        // onChildrenChanged() should be called.
517        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
518    }
519
520    @Test
521    public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed2()
522            throws InterruptedException {
523        // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
524        prepareLooper();
525        final String subscribedMediaId = "subscribedMediaId";
526        final String anotherMediaId = "anotherMediaId";
527        final int testChildrenCount = 101;
528        final CountDownLatch latch = new CountDownLatch(1);
529
530        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
531            @Override
532            public void onSubscribe(@NonNull MediaLibrarySession session,
533                    @NonNull ControllerInfo controller, @NonNull String parentId,
534                    @Nullable Bundle extras) {
535                if (Process.myUid() == controller.getUid()) {
536                    // Shouldn't trigger onChildrenChanged() for the browser,
537                    // because the browser didn't subscribe this media id.
538                    session.notifyChildrenChanged(
539                            controller, anotherMediaId, testChildrenCount, null);
540                }
541            }
542
543            @Override
544            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
545                    ControllerInfo controller, String parentId, int page, int pageSize,
546                    Bundle extras) {
547                return TestUtils.createPlaylist(testChildrenCount);
548            }
549        };
550        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
551            @Override
552            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
553                    int itemCount, Bundle extras) {
554                // Unexpected call.
555                fail();
556            }
557        };
558
559        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
560        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
561        final MediaBrowser2 browser = (MediaBrowser2) createController(
562                token, true, controllerCallbackProxy);
563        browser.subscribe(subscribedMediaId, null);
564
565        // onChildrenChanged() should not be called.
566        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
567    }
568
569    @Test
570    public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed2()
571            throws InterruptedException {
572        // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
573        prepareLooper();
574        final String expectedParentId = "expectedParentId";
575        final int testChildrenCount = 101;
576        final Bundle testExtras = TestUtils.createTestBundle();
577
578        final CountDownLatch latch = new CountDownLatch(1);
579        final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
580            @Override
581            public void onSubscribe(@NonNull MediaLibrarySession session,
582                    @NonNull ControllerInfo controller, @NonNull String parentId,
583                    @Nullable Bundle extras) {
584                if (Process.myUid() == controller.getUid()) {
585                    // Should trigger onChildrenChanged() for the browser.
586                    session.notifyChildrenChanged(
587                            controller, expectedParentId, testChildrenCount, testExtras);
588                }
589            }
590
591            @Override
592            public List<MediaItem2> onGetChildren(MediaLibrarySession session,
593                    ControllerInfo controller, String parentId, int page, int pageSize,
594                    Bundle extras) {
595                return TestUtils.createPlaylist(testChildrenCount);
596            }
597        };
598        final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
599            @Override
600            public void onChildrenChanged(MediaBrowser2 browser, String parentId,
601                    int itemCount, Bundle extras) {
602                assertEquals(expectedParentId, parentId);
603                assertEquals(testChildrenCount, itemCount);
604                assertTrue(TestUtils.equals(testExtras, extras));
605                latch.countDown();
606            }
607        };
608
609        TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
610        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
611        final MediaBrowser2 browser = (MediaBrowser2) createController(
612                token, true, controllerCallbackProxy);
613        browser.subscribe(expectedParentId, null);
614
615        // onChildrenChanged() should be called.
616        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
617    }
618
619    public static class TestBrowserCallback extends BrowserCallback
620            implements TestControllerCallbackInterface {
621        private final ControllerCallback mCallbackProxy;
622        public final CountDownLatch connectLatch = new CountDownLatch(1);
623        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
624        @GuardedBy("this")
625        private Runnable mOnCustomCommandRunnable;
626
627        TestBrowserCallback(ControllerCallback callbackProxy) {
628            if (callbackProxy == null) {
629                callbackProxy = new BrowserCallback() {};
630            }
631            mCallbackProxy = callbackProxy;
632        }
633
634        @CallSuper
635        @Override
636        public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
637            connectLatch.countDown();
638        }
639
640        @CallSuper
641        @Override
642        public void onDisconnected(MediaController2 controller) {
643            disconnectLatch.countDown();
644        }
645
646        @Override
647        public void waitForConnect(boolean expect) throws InterruptedException {
648            if (expect) {
649                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
650            } else {
651                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
652            }
653        }
654
655        @Override
656        public void waitForDisconnect(boolean expect) throws InterruptedException {
657            if (expect) {
658                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
659            } else {
660                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
661            }
662        }
663
664        @Override
665        public void onPlaybackInfoChanged(MediaController2 controller,
666                MediaController2.PlaybackInfo info) {
667            mCallbackProxy.onPlaybackInfoChanged(controller, info);
668        }
669
670        @Override
671        public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
672                Bundle args, ResultReceiver receiver) {
673            mCallbackProxy.onCustomCommand(controller, command, args, receiver);
674            synchronized (this) {
675                if (mOnCustomCommandRunnable != null) {
676                    mOnCustomCommandRunnable.run();
677                }
678            }
679        }
680
681        @Override
682        public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
683            mCallbackProxy.onCustomLayoutChanged(controller, layout);
684        }
685
686        @Override
687        public void onAllowedCommandsChanged(MediaController2 controller,
688                SessionCommandGroup2 commands) {
689            mCallbackProxy.onAllowedCommandsChanged(controller, commands);
690        }
691
692        @Override
693        public void onPlayerStateChanged(MediaController2 controller, int state) {
694            mCallbackProxy.onPlayerStateChanged(controller, state);
695        }
696
697        @Override
698        public void onSeekCompleted(MediaController2 controller, long position) {
699            mCallbackProxy.onSeekCompleted(controller, position);
700        }
701
702        @Override
703        public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
704            mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
705        }
706
707        @Override
708        public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
709                int state) {
710            mCallbackProxy.onBufferingStateChanged(controller, item, state);
711        }
712
713        @Override
714        public void onError(MediaController2 controller, int errorCode, Bundle extras) {
715            mCallbackProxy.onError(controller, errorCode, extras);
716        }
717
718        @Override
719        public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
720            mCallbackProxy.onCurrentMediaItemChanged(controller, item);
721        }
722
723        @Override
724        public void onPlaylistChanged(MediaController2 controller,
725                List<MediaItem2> list, MediaMetadata2 metadata) {
726            mCallbackProxy.onPlaylistChanged(controller, list, metadata);
727        }
728
729        @Override
730        public void onPlaylistMetadataChanged(MediaController2 controller,
731                MediaMetadata2 metadata) {
732            mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
733        }
734
735        @Override
736        public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
737            mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
738        }
739
740        @Override
741        public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
742            mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
743        }
744
745        @Override
746        public void onGetLibraryRootDone(MediaBrowser2 browser, Bundle rootHints,
747                String rootMediaId, Bundle rootExtra) {
748            super.onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
749            if (mCallbackProxy instanceof BrowserCallback) {
750                ((BrowserCallback) mCallbackProxy)
751                        .onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
752            }
753        }
754
755        @Override
756        public void onGetItemDone(MediaBrowser2 browser, String mediaId, MediaItem2 result) {
757            super.onGetItemDone(browser, mediaId, result);
758            if (mCallbackProxy instanceof BrowserCallback) {
759                ((BrowserCallback) mCallbackProxy).onGetItemDone(browser, mediaId, result);
760            }
761        }
762
763        @Override
764        public void onGetChildrenDone(MediaBrowser2 browser, String parentId, int page,
765                int pageSize, List<MediaItem2> result, Bundle extras) {
766            super.onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
767            if (mCallbackProxy instanceof BrowserCallback) {
768                ((BrowserCallback) mCallbackProxy)
769                        .onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
770            }
771        }
772
773        @Override
774        public void onSearchResultChanged(MediaBrowser2 browser, String query, int itemCount,
775                Bundle extras) {
776            super.onSearchResultChanged(browser, query, itemCount, extras);
777            if (mCallbackProxy instanceof BrowserCallback) {
778                ((BrowserCallback) mCallbackProxy)
779                        .onSearchResultChanged(browser, query, itemCount, extras);
780            }
781        }
782
783        @Override
784        public void onGetSearchResultDone(MediaBrowser2 browser, String query, int page,
785                int pageSize, List<MediaItem2> result, Bundle extras) {
786            super.onGetSearchResultDone(browser, query, page, pageSize, result, extras);
787            if (mCallbackProxy instanceof BrowserCallback) {
788                ((BrowserCallback) mCallbackProxy)
789                        .onGetSearchResultDone(browser, query, page, pageSize, result, extras);
790            }
791        }
792
793        @Override
794        public void onChildrenChanged(MediaBrowser2 browser, String parentId, int itemCount,
795                Bundle extras) {
796            super.onChildrenChanged(browser, parentId, itemCount, extras);
797            if (mCallbackProxy instanceof BrowserCallback) {
798                ((BrowserCallback) mCallbackProxy)
799                        .onChildrenChanged(browser, parentId, itemCount, extras);
800            }
801        }
802
803        @Override
804        public void setRunnableForOnCustomCommand(Runnable runnable) {
805            synchronized (this) {
806                mOnCustomCommandRunnable = runnable;
807            }
808        }
809    }
810
811    public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
812        private final BrowserCallback mCallback;
813
814        public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
815                @NonNull BrowserCallback callback) {
816            super(context, token, sHandlerExecutor, callback);
817            mCallback = callback;
818        }
819
820        @Override
821        public BrowserCallback getCallback() {
822            return mCallback;
823        }
824    }
825}
826