1 /* 2 * Copyright (C) 2014 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.cts; 17 18 import android.platform.test.annotations.AppModeFull; 19 import com.android.compatibility.common.util.SystemUtil; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.res.Resources; 26 import android.media.MediaSession2; 27 import android.media.Session2CommandGroup; 28 import android.media.Session2Token; 29 import android.media.session.MediaController; 30 import android.media.session.MediaSession; 31 import android.media.session.MediaSessionManager; 32 import android.media.session.PlaybackState; 33 import android.os.Handler; 34 import android.os.HandlerThread; 35 import android.os.Looper; 36 import android.os.Process; 37 import android.test.InstrumentationTestCase; 38 import android.test.UiThreadTest; 39 import android.view.KeyEvent; 40 41 import java.io.IOException; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.Executor; 46 import java.util.concurrent.TimeUnit; 47 48 @AppModeFull(reason = "TODO: evaluate and port to instant") 49 public class MediaSessionManagerTest extends InstrumentationTestCase { 50 private static final String TAG = "MediaSessionManagerTest"; 51 private static final int TIMEOUT_MS = 3000; 52 private static final int WAIT_MS = 500; 53 54 private MediaSessionManager mSessionManager; 55 56 @Override setUp()57 protected void setUp() throws Exception { 58 super.setUp(); 59 mSessionManager = (MediaSessionManager) getInstrumentation().getTargetContext() 60 .getSystemService(Context.MEDIA_SESSION_SERVICE); 61 } 62 63 @Override tearDown()64 protected void tearDown() throws Exception { 65 super.tearDown(); 66 } 67 testGetActiveSessions()68 public void testGetActiveSessions() throws Exception { 69 try { 70 List<MediaController> controllers = mSessionManager.getActiveSessions(null); 71 fail("Expected security exception for unauthorized call to getActiveSessions"); 72 } catch (SecurityException e) { 73 // Expected 74 } 75 // TODO enable a notification listener, test again, disable, test again 76 } 77 78 @UiThreadTest testAddOnActiveSessionsListener()79 public void testAddOnActiveSessionsListener() throws Exception { 80 try { 81 mSessionManager.addOnActiveSessionsChangedListener(null, null); 82 fail("Expected IAE for call to addOnActiveSessionsChangedListener"); 83 } catch (IllegalArgumentException e) { 84 // Expected 85 } 86 87 MediaSessionManager.OnActiveSessionsChangedListener listener 88 = new MediaSessionManager.OnActiveSessionsChangedListener() { 89 @Override 90 public void onActiveSessionsChanged(List<MediaController> controllers) { 91 92 } 93 }; 94 try { 95 mSessionManager.addOnActiveSessionsChangedListener(listener, null); 96 fail("Expected security exception for call to addOnActiveSessionsChangedListener"); 97 } catch (SecurityException e) { 98 // Expected 99 } 100 101 // TODO enable a notification listener, test again, disable, verify 102 // updates stopped 103 } 104 assertKeyEventEquals(KeyEvent lhs, int keyCode, int action, int repeatCount)105 private void assertKeyEventEquals(KeyEvent lhs, int keyCode, int action, int repeatCount) { 106 assertTrue(lhs.getKeyCode() == keyCode 107 && lhs.getAction() == action 108 && lhs.getRepeatCount() == repeatCount); 109 } 110 injectInputEvent(int keyCode, boolean longPress)111 private void injectInputEvent(int keyCode, boolean longPress) throws IOException { 112 // Injecting key with instrumentation requires a window/view, but we don't have it. 113 // Inject key event through the adb commend to workaround. 114 final String command = "input keyevent " + (longPress ? "--longpress " : "") + keyCode; 115 SystemUtil.runShellCommand(getInstrumentation(), command); 116 } 117 testSetOnVolumeKeyLongPressListener()118 public void testSetOnVolumeKeyLongPressListener() throws Exception { 119 Context context = getInstrumentation().getTargetContext(); 120 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) 121 || context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) 122 || context.getResources().getBoolean(Resources.getSystem().getIdentifier( 123 "config_handleVolumeKeysInWindowManager", "bool", "android"))) { 124 // Skip this test, because the PhoneWindowManager dispatches volume key 125 // events directly to the audio service to change the system volume. 126 return; 127 } 128 Handler handler = createHandler(); 129 130 // Ensure that the listener is called for long-press. 131 VolumeKeyLongPressListener listener = new VolumeKeyLongPressListener(3, handler); 132 mSessionManager.setOnVolumeKeyLongPressListener(listener, handler); 133 injectInputEvent(KeyEvent.KEYCODE_VOLUME_DOWN, true); 134 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 135 assertEquals(listener.mKeyEvents.size(), 3); 136 assertKeyEventEquals(listener.mKeyEvents.get(0), 137 KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, 0); 138 assertKeyEventEquals(listener.mKeyEvents.get(1), 139 KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, 1); 140 assertKeyEventEquals(listener.mKeyEvents.get(2), 141 KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, 0); 142 143 // Ensure the the listener isn't called for short-press. 144 listener = new VolumeKeyLongPressListener(1, handler); 145 mSessionManager.setOnVolumeKeyLongPressListener(listener, handler); 146 injectInputEvent(KeyEvent.KEYCODE_VOLUME_DOWN, false); 147 assertFalse(listener.mCountDownLatch.await(WAIT_MS, TimeUnit.MILLISECONDS)); 148 assertEquals(listener.mKeyEvents.size(), 0); 149 150 // Ensure that the listener isn't called anymore. 151 mSessionManager.setOnVolumeKeyLongPressListener(null, handler); 152 injectInputEvent(KeyEvent.KEYCODE_VOLUME_DOWN, true); 153 assertFalse(listener.mCountDownLatch.await(WAIT_MS, TimeUnit.MILLISECONDS)); 154 assertEquals(listener.mKeyEvents.size(), 0); 155 156 removeHandler(handler); 157 } 158 testSetOnMediaKeyListener()159 public void testSetOnMediaKeyListener() throws Exception { 160 Handler handler = createHandler(); 161 MediaSession session = null; 162 try { 163 session = new MediaSession(getInstrumentation().getTargetContext(), TAG); 164 MediaSessionCallback callback = new MediaSessionCallback(2, session); 165 session.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS 166 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 167 session.setCallback(callback, handler); 168 PlaybackState state = new PlaybackState.Builder() 169 .setState(PlaybackState.STATE_PLAYING, 0, 1.0f).build(); 170 // Fake the media session service so this session can take the media key events. 171 session.setPlaybackState(state); 172 session.setActive(true); 173 174 // A media playback is also needed to receive media key events. 175 Utils.assertMediaPlaybackStarted(getInstrumentation().getTargetContext()); 176 177 // Ensure that the listener is called for media key event, 178 // and any other media sessions don't get the key. 179 MediaKeyListener listener = new MediaKeyListener(2, true, handler); 180 mSessionManager.setOnMediaKeyListener(listener, handler); 181 injectInputEvent(KeyEvent.KEYCODE_HEADSETHOOK, false); 182 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 183 assertEquals(listener.mKeyEvents.size(), 2); 184 assertKeyEventEquals(listener.mKeyEvents.get(0), 185 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_DOWN, 0); 186 assertKeyEventEquals(listener.mKeyEvents.get(1), 187 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_UP, 0); 188 assertFalse(callback.mCountDownLatch.await(WAIT_MS, TimeUnit.MILLISECONDS)); 189 assertEquals(callback.mKeyEvents.size(), 0); 190 191 // Ensure that the listener is called for media key event, 192 // and another media session gets the key. 193 listener = new MediaKeyListener(2, false, handler); 194 mSessionManager.setOnMediaKeyListener(listener, handler); 195 injectInputEvent(KeyEvent.KEYCODE_HEADSETHOOK, false); 196 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 197 assertEquals(listener.mKeyEvents.size(), 2); 198 assertKeyEventEquals(listener.mKeyEvents.get(0), 199 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_DOWN, 0); 200 assertKeyEventEquals(listener.mKeyEvents.get(1), 201 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_UP, 0); 202 assertTrue(callback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 203 assertEquals(callback.mKeyEvents.size(), 2); 204 assertKeyEventEquals(callback.mKeyEvents.get(0), 205 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_DOWN, 0); 206 assertKeyEventEquals(callback.mKeyEvents.get(1), 207 KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.ACTION_UP, 0); 208 209 // Ensure that the listener isn't called anymore. 210 listener = new MediaKeyListener(1, true, handler); 211 mSessionManager.setOnMediaKeyListener(listener, handler); 212 mSessionManager.setOnMediaKeyListener(null, handler); 213 injectInputEvent(KeyEvent.KEYCODE_HEADSETHOOK, false); 214 assertFalse(listener.mCountDownLatch.await(WAIT_MS, TimeUnit.MILLISECONDS)); 215 assertEquals(listener.mKeyEvents.size(), 0); 216 } finally { 217 if (session != null) { 218 session.release(); 219 } 220 removeHandler(handler); 221 } 222 } 223 testRemoteUserInfo()224 public void testRemoteUserInfo() throws Exception { 225 final Context context = getInstrumentation().getTargetContext(); 226 Handler handler = createHandler(); 227 228 MediaSession session = null; 229 try { 230 session = new MediaSession(context , TAG); 231 MediaSessionCallback callback = new MediaSessionCallback(5, session); 232 session.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS 233 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); 234 session.setCallback(callback, handler); 235 PlaybackState state = new PlaybackState.Builder() 236 .setState(PlaybackState.STATE_PLAYING, 0, 1.0f).build(); 237 // Fake the media session service so this session can take the media key events. 238 session.setPlaybackState(state); 239 session.setActive(true); 240 241 // A media playback is also needed to receive media key events. 242 Utils.assertMediaPlaybackStarted(context); 243 244 // Dispatch key events 5 times. 245 KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); 246 // (1), (2): dispatch through key -- this will trigger event twice for up & down. 247 injectInputEvent(KeyEvent.KEYCODE_HEADSETHOOK, false); 248 // (3): dispatch through controller 249 session.getController().dispatchMediaButtonEvent(event); 250 251 // Creating another controller. 252 MediaController controller = new MediaController(context, session.getSessionToken()); 253 // (4): dispatch through different controller. 254 controller.dispatchMediaButtonEvent(event); 255 // (5): dispatch through the same controller 256 controller.dispatchMediaButtonEvent(event); 257 258 // Wait. 259 assertTrue(callback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 260 261 // Caller of (1) ~ (4) shouldn't be the same as any others. 262 for (int i = 0; i < 4; i ++) { 263 for (int j = 0; j < i; j++) { 264 assertNotSame(callback.mCallers.get(i), callback.mCallers.get(j)); 265 } 266 } 267 // Caller of (5) should be the same as (4), since they're called from the same 268 assertEquals(callback.mCallers.get(3), callback.mCallers.get(4)); 269 } finally { 270 if (session != null) { 271 session.release(); 272 } 273 removeHandler(handler); 274 } 275 } 276 testNotifySession2Created()277 public void testNotifySession2Created() throws Exception { 278 final Context context = getInstrumentation().getTargetContext(); 279 Session2Token token = new Session2Token(context, 280 new ComponentName(context, this.getClass())); 281 282 try { 283 mSessionManager.notifySession2Created(token); 284 fail("Expected IllegalArgumentException for a call to notifySession2Created with " + 285 "TYPE_SESSION_SERVICE token"); 286 } catch (IllegalArgumentException e) { 287 // Expected 288 } 289 } 290 testGetSession2Tokens()291 public void testGetSession2Tokens() throws Exception { 292 final Context context = getInstrumentation().getTargetContext(); 293 Handler handler = createHandler(); 294 Executor handlerExecutor = (runnable) -> { 295 if (handler != null) { 296 handler.post(() -> { 297 runnable.run(); 298 }); 299 } 300 }; 301 302 Session2TokenListener listener = new Session2TokenListener(); 303 mSessionManager.addOnSession2TokensChangedListener(listener, handler); 304 305 Session2Callback sessionCallback = new Session2Callback(); 306 try (MediaSession2 session = new MediaSession2.Builder(context) 307 .setSessionCallback(handlerExecutor, sessionCallback) 308 .build()) { 309 assertTrue(sessionCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 310 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 311 312 Session2Token currentToken = session.getToken(); 313 assertTrue(listContainsToken(listener.mTokens, currentToken)); 314 assertTrue(listContainsToken(mSessionManager.getSession2Tokens(), currentToken)); 315 } 316 } 317 testGetSession2TokensWithTwoSessions()318 public void testGetSession2TokensWithTwoSessions() throws Exception { 319 final Context context = getInstrumentation().getTargetContext(); 320 Handler handler = createHandler(); 321 Executor handlerExecutor = (runnable) -> { 322 if (handler != null) { 323 handler.post(() -> { 324 runnable.run(); 325 }); 326 } 327 }; 328 329 Session2TokenListener listener = new Session2TokenListener(); 330 mSessionManager.addOnSession2TokensChangedListener(listener, handler); 331 332 try (MediaSession2 session1 = new MediaSession2.Builder(context) 333 .setSessionCallback(handlerExecutor, new Session2Callback()) 334 .setId("testGetSession2TokensWithTwoSessions_session1") 335 .build()) { 336 337 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 338 Session2Token session1Token = session1.getToken(); 339 assertTrue(listContainsToken(mSessionManager.getSession2Tokens(), session1Token)); 340 341 // Create another session and check the result of getSession2Token(). 342 listener.resetCountDownLatch(); 343 Session2Token session2Token = null; 344 try (MediaSession2 session2 = new MediaSession2.Builder(context) 345 .setSessionCallback(handlerExecutor, new Session2Callback()) 346 .setId("testGetSession2TokensWithTwoSessions_session2") 347 .build()) { 348 349 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 350 session2Token = session2.getToken(); 351 assertNotNull(session2Token); 352 assertTrue(listContainsToken(mSessionManager.getSession2Tokens(), session1Token)); 353 assertTrue(listContainsToken(mSessionManager.getSession2Tokens(), session2Token)); 354 355 listener.resetCountDownLatch(); 356 } 357 358 // Since the session2 is closed, getSession2Tokens() shouldn't include session2's token. 359 assertTrue(listener.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 360 assertTrue(listContainsToken(mSessionManager.getSession2Tokens(), session1Token)); 361 assertFalse(listContainsToken(mSessionManager.getSession2Tokens(), session2Token)); 362 } 363 } 364 testAddAndRemoveSession2TokensListener()365 public void testAddAndRemoveSession2TokensListener() throws Exception { 366 final Context context = getInstrumentation().getTargetContext(); 367 Handler handler = createHandler(); 368 Executor handlerExecutor = (runnable) -> { 369 if (handler != null) { 370 handler.post(() -> { 371 runnable.run(); 372 }); 373 } 374 }; 375 376 Session2TokenListener listener1 = new Session2TokenListener(); 377 mSessionManager.addOnSession2TokensChangedListener(listener1, handler); 378 379 Session2Callback sessionCallback = new Session2Callback(); 380 try (MediaSession2 session = new MediaSession2.Builder(context) 381 .setSessionCallback(handlerExecutor, sessionCallback) 382 .build()) { 383 assertTrue(listener1.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 384 Session2Token currentToken = session.getToken(); 385 assertTrue(listContainsToken(listener1.mTokens, currentToken)); 386 387 // Test removing listener 388 listener1.resetCountDownLatch(); 389 Session2TokenListener listener2 = new Session2TokenListener(); 390 mSessionManager.addOnSession2TokensChangedListener(listener2, handler); 391 mSessionManager.removeOnSession2TokensChangedListener(listener1); 392 393 session.close(); 394 assertFalse(listener1.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 395 assertTrue(listener2.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); 396 } 397 } 398 listContainsToken(List<Session2Token> tokens, Session2Token token)399 private boolean listContainsToken(List<Session2Token> tokens, Session2Token token) { 400 for (int i = 0; i < tokens.size(); i++) { 401 if (tokens.get(i).equals(token)) { 402 return true; 403 } 404 } 405 return false; 406 } 407 createHandler()408 private Handler createHandler() { 409 HandlerThread handlerThread = new HandlerThread("MediaSessionManagerTest"); 410 handlerThread.start(); 411 return new Handler(handlerThread.getLooper()); 412 } 413 removeHandler(Handler handler)414 private void removeHandler(Handler handler) { 415 if (handler == null) { 416 return; 417 } 418 handler.getLooper().quitSafely(); 419 } 420 421 private class VolumeKeyLongPressListener 422 implements MediaSessionManager.OnVolumeKeyLongPressListener { 423 private final List<KeyEvent> mKeyEvents = new ArrayList<>(); 424 private final CountDownLatch mCountDownLatch; 425 private final Handler mHandler; 426 VolumeKeyLongPressListener(int count, Handler handler)427 public VolumeKeyLongPressListener(int count, Handler handler) { 428 mCountDownLatch = new CountDownLatch(count); 429 mHandler = handler; 430 } 431 432 @Override onVolumeKeyLongPress(KeyEvent event)433 public void onVolumeKeyLongPress(KeyEvent event) { 434 mKeyEvents.add(event); 435 // Ensure the listener is called on the thread. 436 assertEquals(mHandler.getLooper(), Looper.myLooper()); 437 mCountDownLatch.countDown(); 438 } 439 } 440 441 private class MediaKeyListener implements MediaSessionManager.OnMediaKeyListener { 442 private final CountDownLatch mCountDownLatch; 443 private final boolean mConsume; 444 private final Handler mHandler; 445 private final List<KeyEvent> mKeyEvents = new ArrayList<>(); 446 MediaKeyListener(int count, boolean consume, Handler handler)447 public MediaKeyListener(int count, boolean consume, Handler handler) { 448 mCountDownLatch = new CountDownLatch(count); 449 mConsume = consume; 450 mHandler = handler; 451 } 452 453 @Override onMediaKey(KeyEvent event)454 public boolean onMediaKey(KeyEvent event) { 455 mKeyEvents.add(event); 456 // Ensure the listener is called on the thread. 457 assertEquals(mHandler.getLooper(), Looper.myLooper()); 458 mCountDownLatch.countDown(); 459 return mConsume; 460 } 461 } 462 463 private class MediaSessionCallback extends MediaSession.Callback { 464 private final CountDownLatch mCountDownLatch; 465 private final MediaSession mSession; 466 private final List<KeyEvent> mKeyEvents = new ArrayList<>(); 467 private final List<MediaSessionManager.RemoteUserInfo> mCallers = new ArrayList<>(); 468 MediaSessionCallback(int count, MediaSession session)469 private MediaSessionCallback(int count, MediaSession session) { 470 mCountDownLatch = new CountDownLatch(count); 471 mSession = session; 472 } 473 onMediaButtonEvent(Intent mediaButtonIntent)474 public boolean onMediaButtonEvent(Intent mediaButtonIntent) { 475 KeyEvent event = (KeyEvent) mediaButtonIntent.getParcelableExtra( 476 Intent.EXTRA_KEY_EVENT); 477 assertNotNull(event); 478 mKeyEvents.add(event); 479 mCallers.add(mSession.getCurrentControllerInfo()); 480 mCountDownLatch.countDown(); 481 return true; 482 } 483 } 484 485 private class Session2Callback extends MediaSession2.SessionCallback { 486 private CountDownLatch mCountDownLatch; 487 Session2Callback()488 private Session2Callback() { 489 mCountDownLatch = new CountDownLatch(1); 490 } 491 492 @Override onConnect(MediaSession2 session, MediaSession2.ControllerInfo controller)493 public Session2CommandGroup onConnect(MediaSession2 session, 494 MediaSession2.ControllerInfo controller) { 495 if (controller.getUid() == Process.SYSTEM_UID) { 496 // System server will try to connect here for monitor session. 497 mCountDownLatch.countDown(); 498 } 499 return new Session2CommandGroup.Builder().build(); 500 } 501 } 502 503 private class Session2TokenListener implements 504 MediaSessionManager.OnSession2TokensChangedListener { 505 private CountDownLatch mCountDownLatch; 506 private List<Session2Token> mTokens; 507 Session2TokenListener()508 private Session2TokenListener() { 509 mCountDownLatch = new CountDownLatch(1); 510 } 511 512 @Override onSession2TokensChanged(List<Session2Token> tokens)513 public void onSession2TokensChanged(List<Session2Token> tokens) { 514 mTokens = tokens; 515 mCountDownLatch.countDown(); 516 } 517 resetCountDownLatch()518 public void resetCountDownLatch() { 519 mCountDownLatch = new CountDownLatch(1); 520 } 521 } 522 } 523