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