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