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