1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assert.fail; 24 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.media.MediaController2; 28 import android.media.MediaMetadata; 29 import android.media.MediaSession2; 30 import android.media.Session2Command; 31 import android.media.Session2CommandGroup; 32 import android.media.Session2Token; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.Process; 37 38 import androidx.test.InstrumentationRegistry; 39 import androidx.test.filters.SmallTest; 40 import androidx.test.runner.AndroidJUnit4; 41 42 import org.junit.After; 43 import org.junit.AfterClass; 44 import org.junit.Before; 45 import org.junit.BeforeClass; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.Executor; 53 import java.util.concurrent.Executors; 54 import java.util.concurrent.TimeUnit; 55 56 /** 57 * Tests {@link android.media.MediaController2}. 58 */ 59 @RunWith(AndroidJUnit4.class) 60 @SmallTest 61 public class MediaController2Test { 62 private static final long WAIT_TIME_MS = 100L; 63 64 static final Object sTestLock = new Object(); 65 66 static final int ALLOWED_COMMAND_CODE = 100; 67 static final Session2CommandGroup SESSION_ALLOWED_COMMANDS = new Session2CommandGroup.Builder() 68 .addCommand(new Session2Command(ALLOWED_COMMAND_CODE)).build(); 69 static final int SESSION_RESULT_CODE = 101; 70 static final String SESSION_RESULT_KEY = "test_result_key"; 71 static final String SESSION_RESULT_VALUE = "test_result_value"; 72 static final Session2Command.Result SESSION_COMMAND_RESULT; 73 74 private static final String TEST_KEY = "test_key"; 75 private static final String TEST_VALUE = "test_value"; 76 77 static { 78 Bundle resultData = new Bundle(); 79 resultData.putString(SESSION_RESULT_KEY, SESSION_RESULT_VALUE); 80 SESSION_COMMAND_RESULT = new Session2Command.Result(SESSION_RESULT_CODE, resultData); 81 } 82 83 static Handler sHandler; 84 static Executor sHandlerExecutor; 85 86 private Context mContext; 87 private Bundle mExtras; 88 private MediaSession2 mSession; 89 private Session2Callback mSessionCallback; 90 91 @BeforeClass 92 public static void setUpThread() { 93 synchronized (MediaSession2Test.class) { 94 if (sHandler != null) { 95 return; 96 } 97 HandlerThread handlerThread = new HandlerThread("MediaSessionTestBase"); 98 handlerThread.start(); 99 sHandler = new Handler(handlerThread.getLooper()); 100 sHandlerExecutor = (runnable) -> { 101 Handler handler; 102 synchronized (MediaSession2Test.class) { 103 handler = sHandler; 104 } 105 if (handler != null) { 106 handler.post(() -> { 107 synchronized (sTestLock) { 108 runnable.run(); 109 } 110 }); 111 } 112 }; 113 } 114 } 115 116 @AfterClass 117 public static void cleanUpThread() { 118 synchronized (MediaSession2Test.class) { 119 if (sHandler == null) { 120 return; 121 } 122 sHandler.getLooper().quitSafely(); 123 sHandler = null; 124 sHandlerExecutor = null; 125 } 126 } 127 128 @Before 129 public void setUp() throws Exception { 130 mContext = InstrumentationRegistry.getContext(); 131 mSessionCallback = new Session2Callback(); 132 mExtras = new Bundle(); 133 mExtras.putString(TEST_KEY, TEST_VALUE); 134 mSession = new MediaSession2.Builder(mContext) 135 .setSessionCallback(sHandlerExecutor, mSessionCallback) 136 .setExtras(mExtras) 137 .build(); 138 } 139 140 @After 141 public void cleanUp() { 142 if (mSession != null) { 143 mSession.close(); 144 mSession = null; 145 } 146 } 147 148 @Test 149 public void testBuilder_withIllegalArguments() { 150 final Session2Token token = new Session2Token( 151 mContext, new ComponentName(mContext, this.getClass())); 152 153 try { 154 MediaController2.Builder builder = new MediaController2.Builder(null, token); 155 fail("null context shouldn't be accepted!"); 156 } catch (IllegalArgumentException e) { 157 // Expected 158 } 159 160 try { 161 MediaController2.Builder builder = new MediaController2.Builder(mContext, null); 162 fail("null token shouldn't be accepted!"); 163 } catch (IllegalArgumentException e) { 164 // Expected 165 } 166 167 try { 168 MediaController2.Builder builder = new MediaController2.Builder(mContext, token); 169 builder.setConnectionHints(null); 170 fail("null connectionHints shouldn't be accepted!"); 171 } catch (IllegalArgumentException e) { 172 // Expected 173 } 174 175 try { 176 MediaController2.Builder builder = new MediaController2.Builder(mContext, token); 177 builder.setControllerCallback(null, new MediaController2.ControllerCallback() {}); 178 fail("null Executor shouldn't be accepted!"); 179 } catch (IllegalArgumentException e) { 180 // Expected 181 } 182 183 try { 184 MediaController2.Builder builder = new MediaController2.Builder(mContext, token); 185 builder.setControllerCallback(Executors.newSingleThreadExecutor(), null); 186 fail("null ControllerCallback shouldn't be accepted!"); 187 } catch (IllegalArgumentException e) { 188 // Expected 189 } 190 } 191 192 @Test 193 public void testBuilder_setConnectionHints_withFrameworkParcelable() throws Exception { 194 final List<MediaSession2.ControllerInfo> controllerInfoList = new ArrayList<>(); 195 final CountDownLatch latch = new CountDownLatch(1); 196 197 try (MediaSession2 session = new MediaSession2.Builder(mContext) 198 .setId("testBuilder_setConnectionHints_withFrameworkParcelable") 199 .setSessionCallback(sHandlerExecutor, new MediaSession2.SessionCallback() { 200 @Override 201 public Session2CommandGroup onConnect(MediaSession2 session, 202 MediaSession2.ControllerInfo controller) { 203 if (controller.getUid() == Process.myUid()) { 204 controllerInfoList.add(controller); 205 latch.countDown(); 206 return new Session2CommandGroup.Builder().build(); 207 } 208 return null; 209 } 210 }) 211 .build()) { 212 213 final Session2Token frameworkParcelable = new Session2Token( 214 mContext, new ComponentName(mContext, this.getClass())); 215 final String testKey = "test_key"; 216 217 Bundle connectionHints = new Bundle(); 218 connectionHints.putParcelable(testKey, frameworkParcelable); 219 220 MediaController2 controller = new MediaController2.Builder(mContext, session.getToken()) 221 .setConnectionHints(connectionHints) 222 .build(); 223 assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); 224 225 Bundle connectionHintsOut = controllerInfoList.get(0).getConnectionHints(); 226 assertTrue(connectionHintsOut.containsKey(testKey)); 227 assertEquals(frameworkParcelable, connectionHintsOut.getParcelable(testKey)); 228 } 229 } 230 231 @Test 232 public void testBuilder_setConnectionHints_withCustomParcelable() { 233 final Session2Token token = new Session2Token( 234 mContext, new ComponentName(mContext, this.getClass())); 235 final String testKey = "test_key"; 236 final MediaSession2Test.CustomParcelable customParcelable = 237 new MediaSession2Test.CustomParcelable(1); 238 239 Bundle connectionHints = new Bundle(); 240 connectionHints.putParcelable(testKey, customParcelable); 241 242 try (MediaController2 controller = new MediaController2.Builder(mContext, token) 243 .setConnectionHints(connectionHints) 244 .build()) { 245 fail("Custom Parcelables shouldn't be accepted!"); 246 } catch (IllegalArgumentException e) { 247 // Expected 248 } 249 } 250 251 @Test 252 public void testCreatingControllerWithoutCallback() throws Exception { 253 try (MediaController2 controller = 254 new MediaController2.Builder(mContext, mSession.getToken()).build()) { 255 assertTrue(mSessionCallback.mOnConnectedLatch.await( 256 WAIT_TIME_MS, TimeUnit.MILLISECONDS)); 257 assertEquals(mContext.getPackageName(), 258 mSessionCallback.mControllerInfo.getPackageName()); 259 } 260 } 261 262 @Test 263 public void testGetConnectedToken() { 264 Controller2Callback controllerCallback = new Controller2Callback(); 265 try (MediaController2 controller = 266 new MediaController2.Builder(mContext, mSession.getToken()) 267 .setControllerCallback(sHandlerExecutor, controllerCallback) 268 .build()) { 269 assertTrue(controllerCallback.awaitOnConnected(WAIT_TIME_MS)); 270 assertEquals(controller, controllerCallback.mController); 271 assertEquals(mSession.getToken(), controller.getConnectedToken()); 272 273 Bundle extrasFromConnectedSessionToken = 274 controller.getConnectedToken().getExtras(); 275 assertNotNull(extrasFromConnectedSessionToken); 276 assertEquals(TEST_VALUE, extrasFromConnectedSessionToken.getString(TEST_KEY)); 277 } finally { 278 assertTrue(controllerCallback.awaitOnDisconnected(WAIT_TIME_MS)); 279 assertNull(controllerCallback.mController.getConnectedToken()); 280 } 281 } 282 283 @Test 284 public void testCallback_onConnected_onDisconnected() { 285 Controller2Callback controllerCallback = new Controller2Callback(); 286 try (MediaController2 controller = 287 new MediaController2.Builder(mContext, mSession.getToken()) 288 .setControllerCallback(sHandlerExecutor, controllerCallback) 289 .build()) { 290 assertTrue(controllerCallback.awaitOnConnected(WAIT_TIME_MS)); 291 assertEquals(controller, controllerCallback.mController); 292 assertTrue(controllerCallback.mAllowedCommands.hasCommand(ALLOWED_COMMAND_CODE)); 293 } finally { 294 assertTrue(controllerCallback.awaitOnDisconnected(WAIT_TIME_MS)); 295 } 296 } 297 298 @Test 299 public void testCallback_onSessionCommand() { 300 Controller2Callback controllerCallback = new Controller2Callback(); 301 try (MediaController2 controller = 302 new MediaController2.Builder(mContext, mSession.getToken()) 303 .setControllerCallback(sHandlerExecutor, controllerCallback) 304 .build()) { 305 assertTrue(controllerCallback.awaitOnConnected(WAIT_TIME_MS)); 306 307 String commandStr = "test_command"; 308 String commandExtraKey = "test_extra_key"; 309 String commandExtraValue = "test_extra_value"; 310 Bundle commandExtra = new Bundle(); 311 commandExtra.putString(commandExtraKey, commandExtraValue); 312 Session2Command command = new Session2Command(commandStr, commandExtra); 313 314 String commandArgKey = "test_arg_key"; 315 String commandArgValue = "test_arg_value"; 316 Bundle commandArg = new Bundle(); 317 commandArg.putString(commandArgKey, commandArgValue); 318 mSession.sendSessionCommand(mSessionCallback.mControllerInfo, command, commandArg); 319 320 assertTrue(controllerCallback.awaitOnSessionCommand(WAIT_TIME_MS)); 321 assertEquals(controller, controllerCallback.mController); 322 assertEquals(commandStr, controllerCallback.mCommand.getCustomAction()); 323 assertEquals(commandExtraValue, 324 controllerCallback.mCommand.getCustomExtras().getString(commandExtraKey)); 325 assertEquals(commandArgValue, controllerCallback.mCommandArgs.getString(commandArgKey)); 326 } finally { 327 assertTrue(controllerCallback.awaitOnDisconnected(WAIT_TIME_MS)); 328 } 329 } 330 331 @Test 332 public void testCallback_onCommandResult() { 333 Controller2Callback controllerCallback = new Controller2Callback(); 334 try (MediaController2 controller = 335 new MediaController2.Builder(mContext, mSession.getToken()) 336 .setControllerCallback(sHandlerExecutor, controllerCallback) 337 .build()) { 338 assertTrue(controllerCallback.awaitOnConnected(WAIT_TIME_MS)); 339 340 String commandStr = "test_command"; 341 String commandExtraKey = "test_extra_key"; 342 String commandExtraValue = "test_extra_value"; 343 Bundle commandExtra = new Bundle(); 344 commandExtra.putString(commandExtraKey, commandExtraValue); 345 Session2Command command = new Session2Command(commandStr, commandExtra); 346 347 String commandArgKey = "test_arg_key"; 348 String commandArgValue = "test_arg_value"; 349 Bundle commandArg = new Bundle(); 350 commandArg.putString(commandArgKey, commandArgValue); 351 controller.sendSessionCommand(command, commandArg); 352 353 assertTrue(controllerCallback.awaitOnCommandResult(WAIT_TIME_MS)); 354 assertEquals(controller, controllerCallback.mController); 355 assertEquals(SESSION_RESULT_CODE, controllerCallback.mCommandResult.getResultCode()); 356 assertEquals(SESSION_RESULT_VALUE, 357 controllerCallback.mCommandResult.getResultData() 358 .getString(SESSION_RESULT_KEY)); 359 } finally { 360 assertTrue(controllerCallback.awaitOnDisconnected(WAIT_TIME_MS)); 361 } 362 } 363 364 @Test 365 public void testCancelSessionCommand() { 366 Controller2Callback controllerCallback = new Controller2Callback(); 367 try (MediaController2 controller = 368 new MediaController2.Builder(mContext, mSession.getToken()) 369 .setControllerCallback(sHandlerExecutor, controllerCallback) 370 .build()) { 371 assertTrue(controllerCallback.awaitOnConnected(WAIT_TIME_MS)); 372 373 String commandStr = "test_command_"; 374 String commandExtraKey = "test_extra_key_"; 375 String commandExtraValue = "test_extra_value_"; 376 Bundle commandExtra = new Bundle(); 377 commandExtra.putString(commandExtraKey, commandExtraValue); 378 Session2Command command = new Session2Command(commandStr, commandExtra); 379 380 String commandArgKey = "test_arg_key_"; 381 String commandArgValue = "test_arg_value_"; 382 Bundle commandArg = new Bundle(); 383 commandArg.putString(commandArgKey, commandArgValue); 384 synchronized (sTestLock) { 385 Object token = controller.sendSessionCommand(command, commandArg); 386 controller.cancelSessionCommand(token); 387 } 388 assertTrue(controllerCallback.awaitOnCommandResult(WAIT_TIME_MS)); 389 assertEquals(Session2Command.Result.RESULT_INFO_SKIPPED, 390 controllerCallback.mCommandResult.getResultCode()); 391 } finally { 392 assertTrue(controllerCallback.awaitOnDisconnected(WAIT_TIME_MS)); 393 } 394 } 395 396 class Session2Callback extends MediaSession2.SessionCallback { 397 MediaSession2.ControllerInfo mControllerInfo; 398 CountDownLatch mOnConnectedLatch = new CountDownLatch(1); 399 400 @Override 401 public Session2CommandGroup onConnect(MediaSession2 session, 402 MediaSession2.ControllerInfo controller) { 403 if (controller.getUid() != Process.myUid()) { 404 return null; 405 } 406 mControllerInfo = controller; 407 mOnConnectedLatch.countDown(); 408 return SESSION_ALLOWED_COMMANDS; 409 } 410 411 @Override 412 public Session2Command.Result onSessionCommand(MediaSession2 session, 413 MediaSession2.ControllerInfo controller, Session2Command command, Bundle args) { 414 return SESSION_COMMAND_RESULT; 415 } 416 } 417 418 class Controller2Callback extends MediaController2.ControllerCallback { 419 CountDownLatch mOnConnectedLatch = new CountDownLatch(1); 420 CountDownLatch mOnDisconnectedLatch = new CountDownLatch(1); 421 private CountDownLatch mOnSessionCommandLatch = new CountDownLatch(1); 422 private CountDownLatch mOnCommandResultLatch = new CountDownLatch(1); 423 424 MediaController2 mController; 425 Session2Command mCommand; 426 Session2CommandGroup mAllowedCommands; 427 Bundle mCommandArgs; 428 Session2Command.Result mCommandResult; 429 430 public boolean await(long waitMs) { 431 try { 432 return mOnSessionCommandLatch.await(waitMs, TimeUnit.MILLISECONDS); 433 } catch (InterruptedException e) { 434 return false; 435 } 436 } 437 438 @Override 439 public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) { 440 super.onConnected(controller, allowedCommands); 441 mController = controller; 442 mAllowedCommands = allowedCommands; 443 mOnConnectedLatch.countDown(); 444 } 445 446 @Override 447 public void onDisconnected(MediaController2 controller) { 448 super.onDisconnected(controller); 449 mController = controller; 450 mOnDisconnectedLatch.countDown(); 451 } 452 453 @Override 454 public Session2Command.Result onSessionCommand(MediaController2 controller, 455 Session2Command command, Bundle args) { 456 super.onSessionCommand(controller, command, args); 457 mController = controller; 458 mCommand = command; 459 mCommandArgs = args; 460 mOnSessionCommandLatch.countDown(); 461 return SESSION_COMMAND_RESULT; 462 } 463 464 @Override 465 public void onCommandResult(MediaController2 controller, Object token, 466 Session2Command command, Session2Command.Result result) { 467 super.onCommandResult(controller, token, command, result); 468 mController = controller; 469 mCommand = command; 470 mCommandResult = result; 471 mOnCommandResultLatch.countDown(); 472 } 473 474 @Override 475 public void onPlaybackActiveChanged(MediaController2 controller, boolean playbackActive) { 476 super.onPlaybackActiveChanged(controller, playbackActive); 477 } 478 479 public boolean awaitOnConnected(long waitTimeMs) { 480 try { 481 return mOnConnectedLatch.await(waitTimeMs, TimeUnit.MILLISECONDS); 482 } catch (InterruptedException e) { 483 return false; 484 } 485 } 486 487 public boolean awaitOnDisconnected(long waitTimeMs) { 488 try { 489 return mOnDisconnectedLatch.await(waitTimeMs, TimeUnit.MILLISECONDS); 490 } catch (InterruptedException e) { 491 return false; 492 } 493 } 494 495 public boolean awaitOnSessionCommand(long waitTimeMs) { 496 try { 497 return mOnSessionCommandLatch.await(waitTimeMs, TimeUnit.MILLISECONDS); 498 } catch (InterruptedException e) { 499 return false; 500 } 501 } 502 503 public boolean awaitOnCommandResult(long waitTimeMs) { 504 try { 505 return mOnCommandResultLatch.await(waitTimeMs, TimeUnit.MILLISECONDS); 506 } catch (InterruptedException e) { 507 return false; 508 } 509 } 510 } 511 } 512