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 android.media.bettertogether.cts.MediaBrowserServiceTestService.KEY_PARENT_MEDIA_ID;
19 import static android.media.bettertogether.cts.MediaBrowserServiceTestService.KEY_SERVICE_COMPONENT_NAME;
20 import static android.media.bettertogether.cts.MediaBrowserServiceTestService.TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED;
21 import static android.media.bettertogether.cts.MediaSessionTestService.KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS;
22 import static android.media.bettertogether.cts.MediaSessionTestService.STEP_CHECK;
23 import static android.media.bettertogether.cts.MediaSessionTestService.STEP_CLEAN_UP;
24 import static android.media.bettertogether.cts.MediaSessionTestService.STEP_SET_UP;
25 import static android.media.browse.MediaBrowser.MediaItem.FLAG_PLAYABLE;
26 import static android.media.cts.Utils.compareRemoteUserInfo;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 import static com.google.common.truth.Truth.assertWithMessage;
30 
31 import static org.junit.Assert.assertThrows;
32 
33 import android.app.Instrumentation;
34 import android.content.ComponentName;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.ServiceConnection;
38 import android.media.MediaDescription;
39 import android.media.browse.MediaBrowser;
40 import android.media.browse.MediaBrowser.MediaItem;
41 import android.media.session.MediaController;
42 import android.media.session.MediaSession;
43 import android.media.session.MediaSessionManager.RemoteUserInfo;
44 import android.os.Bundle;
45 import android.os.IBinder;
46 import android.os.Process;
47 import android.platform.test.annotations.AppModeNonSdkSandbox;
48 import android.platform.test.annotations.RequiresFlagsEnabled;
49 import android.platform.test.flag.junit.CheckFlagsRule;
50 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
51 import android.service.media.MediaBrowserService;
52 import android.service.media.MediaBrowserService.BrowserRoot;
53 
54 import androidx.test.core.app.ApplicationProvider;
55 import androidx.test.ext.junit.runners.AndroidJUnit4;
56 import androidx.test.platform.app.InstrumentationRegistry;
57 
58 import com.android.compatibility.common.util.FrameworkSpecificTest;
59 import com.android.compatibility.common.util.NonMainlineTest;
60 import com.android.media.flags.Flags;
61 
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 
68 import java.util.ArrayList;
69 import java.util.List;
70 import java.util.Objects;
71 import java.util.concurrent.CountDownLatch;
72 import java.util.concurrent.TimeUnit;
73 
74 /**
75  * Test {@link android.service.media.MediaBrowserService}.
76  */
77 @FrameworkSpecificTest
78 @NonMainlineTest
79 @RunWith(AndroidJUnit4.class)
80 @AppModeNonSdkSandbox(reason = "SDK sandbox does not need MediaBrowser.")
81 public class MediaBrowserServiceTest {
82 
83     @Rule
84     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
85 
86     @Rule public final ResourceReleaser mResourceReleaser = new ResourceReleaser();
87 
88     // The maximum time to wait for an operation.
89     private static final long TIME_OUT_MS = 3000L;
90     private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
91     private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
92             "android.media.bettertogether.cts",
93             "android.media.bettertogether.cts.StubMediaBrowserService");
94 
95     private final TestCountDownLatch mOnChildrenLoadedLatch = new TestCountDownLatch();
96     private final TestCountDownLatch mOnChildrenLoadedWithOptionsLatch = new TestCountDownLatch();
97     private final TestCountDownLatch mOnItemLoadedLatch = new TestCountDownLatch();
98 
99     private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
100             new MediaBrowser.SubscriptionCallback() {
101             @Override
102             public void onChildrenLoaded(String parentId, List<MediaItem> children) {
103                 if (children != null) {
104                     for (MediaItem item : children) {
105                         assertRootHints(item);
106                     }
107                 }
108                 mOnChildrenLoadedLatch.countDown();
109             }
110 
111             @Override
112             public void onChildrenLoaded(String parentId, List<MediaItem> children,
113                     Bundle options) {
114                 if (children != null) {
115                     for (MediaItem item : children) {
116                         assertRootHints(item);
117                     }
118                 }
119                 mOnChildrenLoadedWithOptionsLatch.countDown();
120             }
121         };
122 
123     private final MediaBrowser.ItemCallback mItemCallback = new MediaBrowser.ItemCallback() {
124         @Override
125         public void onItemLoaded(MediaItem item) {
126             assertRootHints(item);
127             mOnItemLoadedLatch.countDown();
128         }
129     };
130 
131     private Context mContext;
132     private MediaBrowser mMediaBrowser;
133     private RemoteUserInfo mBrowserInfo;
134     private StubMediaBrowserService mMediaBrowserService;
135     private Bundle mRootHints;
136 
getInstrumentation()137     private Instrumentation getInstrumentation() {
138         return InstrumentationRegistry.getInstrumentation();
139     }
140 
141     @Before
setUp()142     public void setUp() throws Exception {
143         mContext = getInstrumentation().getTargetContext();
144         mRootHints = new Bundle();
145         mRootHints.putBoolean(BrowserRoot.EXTRA_RECENT, true);
146         mRootHints.putBoolean(BrowserRoot.EXTRA_OFFLINE, true);
147         mRootHints.putBoolean(BrowserRoot.EXTRA_SUGGESTED, true);
148         mBrowserInfo =
149                 new RemoteUserInfo(mContext.getPackageName(), Process.myPid(), Process.myUid());
150         mOnChildrenLoadedLatch.reset();
151         mOnChildrenLoadedWithOptionsLatch.reset();
152         mOnItemLoadedLatch.reset();
153 
154         final CountDownLatch onConnectedLatch = new CountDownLatch(1);
155         getInstrumentation().runOnMainSync(()-> {
156             mMediaBrowser = new MediaBrowser(mContext,
157                 TEST_BROWSER_SERVICE, new MediaBrowser.ConnectionCallback() {
158                     @Override
159                     public void onConnected() {
160                         mMediaBrowserService = StubMediaBrowserService.sInstance;
161                         onConnectedLatch.countDown();
162                     }
163                 }, mRootHints);
164             mMediaBrowser.connect();
165         });
166         assertThat(onConnectedLatch.await(TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue();
167         assertThat(mMediaBrowserService).isNotNull();
168     }
169 
170     @After
tearDown()171     public void tearDown() {
172         getInstrumentation().runOnMainSync(()-> {
173             if (mMediaBrowser != null) {
174                 mMediaBrowser.disconnect();
175                 mMediaBrowser = null;
176             }
177         });
178     }
179 
180     @Test
testGetSessionToken()181     public void testGetSessionToken() {
182         assertThat(mMediaBrowserService.getSessionToken())
183                 .isEqualTo(StubMediaBrowserService.sSession.getSessionToken());
184     }
185 
186     @Test
testNotifyChildrenChanged()187     public void testNotifyChildrenChanged() throws Exception {
188         getInstrumentation().runOnMainSync(()-> {
189             mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, mSubscriptionCallback);
190         });
191         assertThat(mOnChildrenLoadedLatch.await(TIME_OUT_MS)).isTrue();
192 
193         mOnChildrenLoadedLatch.reset();
194         mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserService.MEDIA_ID_ROOT);
195         assertThat(mOnChildrenLoadedLatch.await(TIME_OUT_MS)).isTrue();
196     }
197 
198     @Test
testNotifyChildrenChangedWithNullOptionsThrowsIAE()199     public void testNotifyChildrenChangedWithNullOptionsThrowsIAE() {
200         assertThrows(IllegalArgumentException.class,
201                 () -> mMediaBrowserService.notifyChildrenChanged(
202                         StubMediaBrowserService.MEDIA_ID_ROOT, /*options=*/ null));
203     }
204 
205     @Test
testNotifyChildrenChangedWithPagination()206     public void testNotifyChildrenChangedWithPagination() {
207         final int pageSize = 5;
208         final int page = 2;
209         Bundle options = new Bundle();
210         options.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
211         options.putInt(MediaBrowser.EXTRA_PAGE, page);
212 
213         getInstrumentation().runOnMainSync(()-> {
214             mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, options,
215                     mSubscriptionCallback);
216         });
217         assertThat(mOnChildrenLoadedWithOptionsLatch.await(TIME_OUT_MS)).isTrue();
218 
219         mOnChildrenLoadedWithOptionsLatch.reset();
220         mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserService.MEDIA_ID_ROOT);
221         assertThat(mOnChildrenLoadedWithOptionsLatch.await(TIME_OUT_MS)).isTrue();
222 
223         // Notify that the items overlapping with the given options are changed.
224         mOnChildrenLoadedWithOptionsLatch.reset();
225         final int newPageSize = 3;
226         final int overlappingNewPage = pageSize * page / newPageSize;
227         Bundle overlappingOptions = new Bundle();
228         overlappingOptions.putInt(MediaBrowser.EXTRA_PAGE_SIZE, newPageSize);
229         overlappingOptions.putInt(MediaBrowser.EXTRA_PAGE, overlappingNewPage);
230         mMediaBrowserService.notifyChildrenChanged(
231                 StubMediaBrowserService.MEDIA_ID_ROOT, overlappingOptions);
232         assertThat(mOnChildrenLoadedWithOptionsLatch.await(TIME_OUT_MS)).isTrue();
233 
234         // Notify that the items non-overlapping with the given options are changed.
235         mOnChildrenLoadedWithOptionsLatch.reset();
236         Bundle nonOverlappingOptions = new Bundle();
237         nonOverlappingOptions.putInt(MediaBrowser.EXTRA_PAGE_SIZE, pageSize);
238         nonOverlappingOptions.putInt(MediaBrowser.EXTRA_PAGE, page + 1);
239         mMediaBrowserService.notifyChildrenChanged(
240                 StubMediaBrowserService.MEDIA_ID_ROOT, nonOverlappingOptions);
241         assertThat(mOnChildrenLoadedWithOptionsLatch.await(WAIT_TIME_FOR_NO_RESPONSE_MS)).isFalse();
242     }
243 
244     @Test
testDelayedNotifyChildrenChanged()245     public void testDelayedNotifyChildrenChanged() throws Exception {
246         getInstrumentation().runOnMainSync(()-> {
247             mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_CHILDREN_DELAYED,
248                     mSubscriptionCallback);
249         });
250         assertThat(mOnChildrenLoadedLatch.await(WAIT_TIME_FOR_NO_RESPONSE_MS)).isFalse();
251 
252         mMediaBrowserService.sendDelayedNotifyChildrenChanged();
253         assertThat(mOnChildrenLoadedLatch.await(TIME_OUT_MS)).isTrue();
254 
255         mOnChildrenLoadedLatch.reset();
256         mMediaBrowserService.notifyChildrenChanged(
257                 StubMediaBrowserService.MEDIA_ID_CHILDREN_DELAYED);
258         assertThat(mOnChildrenLoadedLatch.await(WAIT_TIME_FOR_NO_RESPONSE_MS)).isFalse();
259 
260         mMediaBrowserService.sendDelayedNotifyChildrenChanged();
261         assertThat(mOnChildrenLoadedLatch.await(TIME_OUT_MS)).isTrue();
262     }
263 
264     @Test
testDelayedItem()265     public void testDelayedItem() throws Exception {
266         getInstrumentation().runOnMainSync(()-> {
267             mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN_DELAYED,
268                     mItemCallback);
269         });
270         assertThat(mOnItemLoadedLatch.await(WAIT_TIME_FOR_NO_RESPONSE_MS)).isFalse();
271 
272         mMediaBrowserService.sendDelayedItemLoaded();
273         assertThat(mOnItemLoadedLatch.await(TIME_OUT_MS)).isTrue();
274     }
275 
276     @Test
testGetBrowserInfo()277     public void testGetBrowserInfo() throws Exception {
278         // StubMediaBrowserService stores the browser info in its onGetRoot().
279         assertThat(compareRemoteUserInfo(mBrowserInfo, StubMediaBrowserService.sBrowserInfo))
280                 .isTrue();
281 
282         StubMediaBrowserService.clearBrowserInfo();
283         getInstrumentation().runOnMainSync(()-> {
284             mMediaBrowser.subscribe(StubMediaBrowserService.MEDIA_ID_ROOT, mSubscriptionCallback);
285         });
286         assertThat(mOnChildrenLoadedLatch.await(TIME_OUT_MS)).isTrue();
287         assertThat(compareRemoteUserInfo(mBrowserInfo, StubMediaBrowserService.sBrowserInfo))
288                 .isTrue();
289 
290         StubMediaBrowserService.clearBrowserInfo();
291         getInstrumentation().runOnMainSync(()-> {
292             mMediaBrowser.getItem(StubMediaBrowserService.MEDIA_ID_CHILDREN[0], mItemCallback);
293         });
294         assertThat(mOnItemLoadedLatch.await(TIME_OUT_MS)).isTrue();
295         assertThat(compareRemoteUserInfo(mBrowserInfo, StubMediaBrowserService.sBrowserInfo))
296                 .isTrue();
297     }
298 
299     @Test
testBrowserRoot()300     public void testBrowserRoot() {
301         final String id = "test-id";
302         final String key = "test-key";
303         final String val = "test-val";
304         final Bundle extras = new Bundle();
305         extras.putString(key, val);
306 
307         MediaBrowserService.BrowserRoot browserRoot = new BrowserRoot(id, extras);
308         assertThat(browserRoot.getRootId()).isEqualTo(id);
309         assertThat(browserRoot.getExtras().getString(key)).isEqualTo(val);
310     }
311 
312     /**
313      * Check that a series of {@link MediaBrowserService#notifyChildrenChanged} does not break
314      * {@link MediaBrowser} on the remote process due to binder buffer overflow.
315      */
316     @Test
testSeriesOfNotifyChildrenChanged()317     public void testSeriesOfNotifyChildrenChanged() throws Exception {
318         String parentMediaId = "testSeriesOfNotifyChildrenChanged";
319         int numberOfCalls = 100;
320         int childrenSize = 1_000;
321         List<MediaItem> children = new ArrayList<>();
322         for (int id = 0; id < childrenSize; id++) {
323             MediaDescription description = new MediaDescription.Builder()
324                     .setMediaId(Integer.toString(id)).build();
325             children.add(new MediaItem(description, FLAG_PLAYABLE));
326         }
327         mMediaBrowserService.putChildrenToMap(parentMediaId, children);
328 
329         try (RemoteService.Invoker invoker = new RemoteService.Invoker(
330                 ApplicationProvider.getApplicationContext(),
331                 MediaBrowserServiceTestService.class,
332                 TEST_SERIES_OF_NOTIFY_CHILDREN_CHANGED)) {
333             Bundle args = new Bundle();
334             args.putParcelable(KEY_SERVICE_COMPONENT_NAME, TEST_BROWSER_SERVICE);
335             args.putString(KEY_PARENT_MEDIA_ID, parentMediaId);
336             args.putInt(KEY_EXPECTED_TOTAL_NUMBER_OF_ITEMS, numberOfCalls * childrenSize);
337             invoker.run(STEP_SET_UP, args);
338             for (int i = 0; i < numberOfCalls; i++) {
339                 mMediaBrowserService.notifyChildrenChanged(parentMediaId);
340             }
341             invoker.run(STEP_CHECK);
342             invoker.run(STEP_CLEAN_UP);
343         }
344 
345         mMediaBrowserService.removeChildrenFromMap(parentMediaId);
346     }
347 
348     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_NULL_SESSION_IN_MEDIA_BROWSER_SERVICE)
349     @Test
testSetNullSessionToken()350     public void testSetNullSessionToken() {
351         MediaBrowserCallbackImpl browserCallback = new MediaBrowserCallbackImpl();
352         ComponentName componentName = new ComponentName(mContext, SimpleMediaBrowserService.class);
353         MediaBrowser browser = createMediaBrowser(browserCallback, componentName);
354         browser.connect();
355         mResourceReleaser.add(browser::disconnect);
356 
357         // We establish a browserless connection to keep the service alive after we call
358         // setSessionToken(null). That way we can test that we can re-populate the session
359         // afterwards. Otherwise, we open ourselves to the service being destroyed immediately
360         // after browsers disconnect as a result setting a null session token.
361         Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
362         intent.setComponent(componentName);
363         PlaceholderServiceConnection connection = new PlaceholderServiceConnection();
364         assertThat(
365                         mContext.bindService(
366                                 intent,
367                                 connection,
368                                 Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES))
369                 .isTrue();
370         mResourceReleaser.add(() -> mContext.unbindService(connection));
371 
372         assertWithMessage("Browser service not created after connecting a browser.")
373                 .that(SimpleMediaBrowserService.sInstanceInitializedCondition.block(TIME_OUT_MS))
374                 .isTrue();
375         MediaBrowserService browserService =
376                 Objects.requireNonNull(SimpleMediaBrowserService.sInstance.get());
377 
378         String firstSessionTag = "tag 1";
379         MediaSession firstSession = new MediaSession(mContext, firstSessionTag);
380         mResourceReleaser.add(firstSession::release);
381         browserService.setSessionToken(firstSession.getSessionToken());
382 
383         browserCallback.waitForConnection();
384 
385         String tag = new MediaController(mContext, browser.getSessionToken()).getTag();
386         assertThat(tag).isEqualTo(firstSessionTag);
387 
388         browserService.setSessionToken(null);
389         assertThat(browserService.getSessionToken()).isNull();
390         browserCallback.waitForDisconnection();
391 
392         String secondSessionTag = "tag 2";
393         MediaSession secondSession = new MediaSession(mContext, secondSessionTag);
394         mResourceReleaser.add(secondSession::release);
395 
396         browserCallback.reset();
397         browserService.setSessionToken(secondSession.getSessionToken());
398         assertThat(browserService.getSessionToken())
399                 .isSameInstanceAs(secondSession.getSessionToken());
400 
401         browser.connect();
402         browserCallback.waitForConnection();
403         tag = new MediaController(mContext, browser.getSessionToken()).getTag();
404         assertThat(tag).isEqualTo(secondSessionTag);
405     }
406 
assertRootHints(MediaItem item)407     private void assertRootHints(MediaItem item) {
408         Bundle rootHints = item.getDescription().getExtras();
409         assertThat(rootHints).isNotNull();
410         assertThat(rootHints.getBoolean(BrowserRoot.EXTRA_RECENT))
411                 .isEqualTo(mRootHints.getBoolean(BrowserRoot.EXTRA_RECENT));
412         assertThat(rootHints.getBoolean(BrowserRoot.EXTRA_OFFLINE))
413                 .isEqualTo(mRootHints.getBoolean(BrowserRoot.EXTRA_OFFLINE));
414         assertThat(rootHints.getBoolean(BrowserRoot.EXTRA_SUGGESTED))
415                 .isEqualTo(mRootHints.getBoolean(BrowserRoot.EXTRA_SUGGESTED));
416     }
417 
createMediaBrowser( MediaBrowserCallbackImpl callback, ComponentName componentName)418     private MediaBrowser createMediaBrowser(
419             MediaBrowserCallbackImpl callback, ComponentName componentName) {
420         MediaBrowser[] browserHolder = new MediaBrowser[1];
421         getInstrumentation()
422                 .runOnMainSync(
423                         () ->
424                                 browserHolder[0] =
425                                         new MediaBrowser(
426                                                 mContext, componentName, callback, mRootHints));
427         return browserHolder[0];
428     }
429 
430     private static class MediaBrowserCallbackImpl extends MediaBrowser.ConnectionCallback {
431 
432         private final TestCountDownLatch mConnectionLatch;
433         private final TestCountDownLatch mDisonnectionLatch;
434 
MediaBrowserCallbackImpl()435         private MediaBrowserCallbackImpl() {
436             mConnectionLatch = new TestCountDownLatch();
437             mDisonnectionLatch = new TestCountDownLatch();
438         }
439 
reset()440         public void reset() {
441             mConnectionLatch.reset();
442             mDisonnectionLatch.reset();
443         }
444 
waitForConnection()445         public void waitForConnection() {
446             assertThat(mConnectionLatch.await(TIME_OUT_MS)).isTrue();
447         }
448 
waitForDisconnection()449         public void waitForDisconnection() {
450             assertThat(mDisonnectionLatch.await(TIME_OUT_MS)).isTrue();
451         }
452 
453         @Override
onConnected()454         public void onConnected() {
455             mConnectionLatch.countDown();
456         }
457 
458         @Override
onConnectionFailed()459         public void onConnectionFailed() {
460             mDisonnectionLatch.countDown();
461         }
462     }
463 
464     /**
465      * A service connection that does nothing.
466      *
467      * <p>Serves the purpose of keeping a service alive.
468      */
469     private static class PlaceholderServiceConnection implements ServiceConnection {
470 
471         @Override
onServiceConnected(ComponentName name, IBinder service)472         public void onServiceConnected(ComponentName name, IBinder service) {}
473 
474         @Override
onServiceDisconnected(ComponentName name)475         public void onServiceDisconnected(ComponentName name) {}
476     }
477 
478     private static class TestCountDownLatch {
479         private CountDownLatch mLatch;
480 
TestCountDownLatch()481         TestCountDownLatch() {
482             mLatch = new CountDownLatch(1);
483         }
484 
reset()485         void reset() {
486             mLatch = new CountDownLatch(1);
487         }
488 
countDown()489         void countDown() {
490             mLatch.countDown();
491         }
492 
await(long timeoutMs)493         boolean await(long timeoutMs) {
494             try {
495                 return mLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
496             } catch (InterruptedException e) {
497                 return false;
498             }
499         }
500     }
501 }
502