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.assertFalse;
21 import static org.junit.Assert.assertNotEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.testng.Assert.assertNull;
26 
27 import android.app.Notification;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.media.MediaController2;
32 import android.media.MediaSession2;
33 import android.media.MediaSession2.ControllerInfo;
34 import android.media.MediaSession2Service;
35 import android.media.Session2CommandGroup;
36 import android.media.Session2Token;
37 import android.os.Bundle;
38 import android.os.HandlerThread;
39 import android.os.Process;
40 
41 import androidx.test.InstrumentationRegistry;
42 import androidx.test.filters.SmallTest;
43 import androidx.test.runner.AndroidJUnit4;
44 
45 import org.junit.After;
46 import org.junit.AfterClass;
47 import org.junit.Before;
48 import org.junit.BeforeClass;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.concurrent.CountDownLatch;
55 import java.util.concurrent.TimeUnit;
56 
57 /**
58  * Tests {@link MediaSession2Service}.
59  */
60 @RunWith(AndroidJUnit4.class)
61 @SmallTest
62 public class MediaSession2ServiceTest {
63     private static final long TIMEOUT_MS = 3000L;
64     private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
65 
66     private static HandlerExecutor sHandlerExecutor;
67     private final List<MediaController2> mControllers = new ArrayList<>();
68     private Context mContext;
69     private Session2Token mToken;
70 
71     @BeforeClass
setUpThread()72     public static void setUpThread() {
73         synchronized (MediaSession2ServiceTest.class) {
74             if (sHandlerExecutor != null) {
75                 return;
76             }
77             HandlerThread handlerThread = new HandlerThread("MediaSession2ServiceTest");
78             handlerThread.start();
79             sHandlerExecutor = new HandlerExecutor(handlerThread.getLooper());
80             StubMediaSession2Service.setHandlerExecutor(sHandlerExecutor);
81         }
82     }
83 
84     @AfterClass
cleanUpThread()85     public static void cleanUpThread() {
86         synchronized (MediaSession2Test.class) {
87             if (sHandlerExecutor == null) {
88                 return;
89             }
90             StubMediaSession2Service.setHandlerExecutor(null);
91             sHandlerExecutor.getLooper().quitSafely();
92             sHandlerExecutor = null;
93         }
94     }
95 
96     @Before
setUp()97     public void setUp() throws Exception {
98         mContext = InstrumentationRegistry.getContext();
99         mToken = new Session2Token(mContext,
100                 new ComponentName(mContext, StubMediaSession2Service.class));
101     }
102 
103     @After
cleanUp()104     public void cleanUp() throws Exception {
105         for (MediaController2 controller : mControllers) {
106             controller.close();
107         }
108         mControllers.clear();
109 
110         StubMediaSession2Service.setTestInjector(null);
111     }
112 
113     /**
114      * Tests whether {@link MediaSession2Service#onGetSession(ControllerInfo)}
115      * is called when controller tries to connect, with the proper arguments.
116      */
117     @Test
testOnGetSessionIsCalled()118     public void testOnGetSessionIsCalled() throws InterruptedException {
119         final List<ControllerInfo> controllerInfoList = new ArrayList<>();
120         final CountDownLatch latch = new CountDownLatch(1);
121         StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
122             @Override
123             MediaSession2 onGetSession(ControllerInfo controllerInfo) {
124                 controllerInfoList.add(controllerInfo);
125                 latch.countDown();
126                 return null;
127             }
128         });
129         Bundle testHints = new Bundle();
130         testHints.putString("test_key", "test_value");
131         MediaController2 controller = new MediaController2.Builder(mContext, mToken)
132                 .setConnectionHints(testHints)
133                 .setControllerCallback(sHandlerExecutor,
134                         new MediaController2.ControllerCallback() {})
135                 .build();
136         mControllers.add(controller);
137 
138         // onGetSession() should be called.
139         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
140         assertEquals(controllerInfoList.get(0).getPackageName(), mContext.getPackageName());
141         assertTrue(TestUtils.equals(controllerInfoList.get(0).getConnectionHints(), testHints));
142     }
143 
144     /**
145      * Tests whether the controller is connected to the session which is returned from
146      * {@link MediaSession2Service#onGetSession(ControllerInfo)}.
147      * Also checks whether the connection hints are properly passed to
148      * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
149      */
150     @Test
testOnGetSession_returnsSession()151     public void testOnGetSession_returnsSession() throws InterruptedException {
152         final List<ControllerInfo> controllerInfoList = new ArrayList<>();
153         final CountDownLatch latch = new CountDownLatch(1);
154 
155         try (MediaSession2 testSession = new MediaSession2.Builder(mContext)
156                 .setId("testOnGetSession_returnsSession")
157                 .setSessionCallback(sHandlerExecutor, new SessionCallback() {
158                     @Override
159                     public Session2CommandGroup onConnect(MediaSession2 session,
160                             ControllerInfo controller) {
161                         if (controller.getUid() == Process.myUid()) {
162                             controllerInfoList.add(controller);
163                             latch.countDown();
164                             return new Session2CommandGroup.Builder().build();
165                         }
166                         return null;
167                     }
168                 }).build()) {
169 
170             StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
171                 @Override
172                 MediaSession2 onGetSession(ControllerInfo controllerInfo) {
173                     // Add fake call for preventing this from being missed by CTS coverage.
174                     super.onGetSession(controllerInfo);
175                     return testSession;
176                 }
177             });
178 
179             Bundle testHints = new Bundle();
180             testHints.putString("test_key", "test_value");
181             MediaController2 controller = new MediaController2.Builder(mContext, mToken)
182                     .setConnectionHints(testHints)
183                     .setControllerCallback(sHandlerExecutor,
184                             new MediaController2.ControllerCallback() {})
185                     .build();
186             mControllers.add(controller);
187 
188             // MediaSession2.SessionCallback#onConnect() should be called.
189             assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
190             assertEquals(controllerInfoList.get(0).getPackageName(), mContext.getPackageName());
191             assertTrue(TestUtils.equals(controllerInfoList.get(0).getConnectionHints(), testHints));
192 
193             // The controller should be connected to the right session.
194             assertNotEquals(mToken, controller.getConnectedToken());
195             assertEquals(testSession.getToken(), controller.getConnectedToken());
196         }
197     }
198 
199     /**
200      * Tests whether {@link MediaSession2Service#onGetSession(ControllerInfo)}
201      * can return different sessions for different controllers.
202      */
203     @Test
testOnGetSession_returnsDifferentSessions()204     public void testOnGetSession_returnsDifferentSessions() throws InterruptedException {
205         final List<Session2Token> tokens = new ArrayList<>();
206         StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
207             @Override
208             MediaSession2 onGetSession(ControllerInfo controllerInfo) {
209                 MediaSession2 session = createMediaSession2(
210                         "testOnGetSession_returnsDifferentSessions" + System.currentTimeMillis());
211                 tokens.add(session.getToken());
212                 return session;
213             }
214         });
215 
216         MediaController2 controller1 = createConnectedController(mToken);
217         MediaController2 controller2 = createConnectedController(mToken);
218 
219         assertNotEquals(mToken, controller1.getConnectedToken());
220         assertNotEquals(mToken, controller2.getConnectedToken());
221 
222         assertNotEquals(controller1.getConnectedToken(),
223                 controller2.getConnectedToken());
224         assertEquals(2, tokens.size());
225         assertEquals(tokens.get(0), controller1.getConnectedToken());
226         assertEquals(tokens.get(1), controller2.getConnectedToken());
227     }
228 
229     /**
230      * Tests whether {@link MediaSession2Service#onGetSession(ControllerInfo)}
231      * can reject incoming connection by returning null.
232      */
233     @Test
testOnGetSession_rejectsConnection()234     public void testOnGetSession_rejectsConnection() throws InterruptedException {
235         StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
236             @Override
237             MediaSession2 onGetSession(ControllerInfo controllerInfo) {
238                 return null;
239             }
240         });
241         final CountDownLatch latch = new CountDownLatch(1);
242         MediaController2 controller = new MediaController2.Builder(mContext, mToken)
243                 .setControllerCallback(sHandlerExecutor, new MediaController2.ControllerCallback() {
244                     @Override
245                     public void onDisconnected(MediaController2 controller) {
246                         latch.countDown();
247                     }
248                 })
249                 .build();
250 
251         // MediaController2.ControllerCallback#onDisconnected() should be called.
252         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
253         assertNull(controller.getConnectedToken());
254     }
255 
256     @Test
testAllControllersDisconnected_oneSession()257     public void testAllControllersDisconnected_oneSession() throws InterruptedException {
258         final CountDownLatch latch = new CountDownLatch(1);
259         final MediaSession2 testSession =
260                 createMediaSession2("testAllControllersDisconnected_oneSession");
261 
262         StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
263             @Override
264             MediaSession2 onGetSession(ControllerInfo controllerInfo) {
265                 return testSession;
266             }
267 
268             @Override
269             void onServiceDestroyed() {
270                 latch.countDown();
271             }
272         });
273         MediaController2 controller1 = createConnectedController(mToken);
274         MediaController2 controller2 = createConnectedController(mToken);
275 
276         controller1.close();
277         assertFalse(latch.await(WAIT_TIME_FOR_NO_RESPONSE_MS, TimeUnit.MILLISECONDS));
278 
279         // Service should be closed only when all controllers are closed.
280         controller2.close();
281         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
282     }
283 
284     @Test
testAllControllersDisconnected_multipleSessions()285     public void testAllControllersDisconnected_multipleSessions() throws InterruptedException {
286         final CountDownLatch latch = new CountDownLatch(1);
287         StubMediaSession2Service.setTestInjector(new StubMediaSession2Service.TestInjector() {
288             @Override
289             MediaSession2 onGetSession(ControllerInfo controllerInfo) {
290                 return createMediaSession2("testAllControllersDisconnected_multipleSession"
291                         + System.currentTimeMillis());
292             }
293 
294             @Override
295             void onServiceDestroyed() {
296                 latch.countDown();
297             }
298         });
299 
300         MediaController2 controller1 = createConnectedController(mToken);
301         MediaController2 controller2 = createConnectedController(mToken);
302 
303         controller1.close();
304         assertFalse(latch.await(WAIT_TIME_FOR_NO_RESPONSE_MS, TimeUnit.MILLISECONDS));
305 
306         // Service should be closed only when all controllers are closed.
307         controller2.close();
308         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
309     }
310 
311     @Test
testGetSessions()312     public void testGetSessions() throws InterruptedException {
313         MediaController2 controller = createConnectedController(mToken);
314         MediaSession2Service service = StubMediaSession2Service.getInstance();
315         try (MediaSession2 session = new MediaSession2.Builder(mContext)
316                 .setId("testGetSessions")
317                 .setSessionCallback(sHandlerExecutor, new SessionCallback())
318                 .build()) {
319             service.addSession(session);
320             List<MediaSession2> sessions = service.getSessions();
321             assertTrue(sessions.contains(session));
322             assertEquals(2, sessions.size());
323 
324             service.removeSession(session);
325             sessions = service.getSessions();
326             assertFalse(sessions.contains(session));
327         }
328     }
329 
330     @Test
testAddSessions_removedWhenClose()331     public void testAddSessions_removedWhenClose() throws InterruptedException {
332         MediaController2 controller = createConnectedController(mToken);
333         MediaSession2Service service = StubMediaSession2Service.getInstance();
334         try (MediaSession2 session = new MediaSession2.Builder(mContext)
335                 .setId("testAddSessions_removedWhenClose")
336                 .setSessionCallback(sHandlerExecutor, new SessionCallback())
337                 .build()) {
338             service.addSession(session);
339             List<MediaSession2> sessions = service.getSessions();
340             assertTrue(sessions.contains(session));
341             assertEquals(2, sessions.size());
342 
343             session.close();
344             sessions = service.getSessions();
345             assertFalse(sessions.contains(session));
346         }
347     }
348 
349     @Test
testOnUpdateNotification()350     public void testOnUpdateNotification() throws InterruptedException {
351         MediaController2 controller = createConnectedController(mToken);
352         MediaSession2Service service = StubMediaSession2Service.getInstance();
353         MediaSession2 testSession = service.getSessions().get(0);
354         CountDownLatch latch = new CountDownLatch(2);
355 
356         StubMediaSession2Service.setTestInjector(
357                 new StubMediaSession2Service.TestInjector() {
358                     @Override
359                     MediaSession2Service.MediaNotification onUpdateNotification(
360                             MediaSession2 session) {
361                         assertEquals(testSession, session);
362                         switch ((int) latch.getCount()) {
363                             case 2:
364 
365                                 break;
366                             case 1:
367                         }
368                         latch.countDown();
369                         return super.onUpdateNotification(session);
370                     }
371                 });
372 
373         testSession.setPlaybackActive(true);
374         testSession.setPlaybackActive(false);
375         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
376 
377         // Add fake call for preventing this from being missed by CTS coverage.
378         if (StubMediaSession2Service.getInstance() != null) {
379             ((MediaSession2Service) StubMediaSession2Service.getInstance())
380                     .onUpdateNotification(null);
381         }
382     }
383 
384     @Test
testOnBind()385     public void testOnBind() throws Exception {
386         MediaController2 controller1 = createConnectedController(mToken);
387         MediaSession2Service service = StubMediaSession2Service.getInstance();
388 
389         Intent serviceIntent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
390         assertNotNull(service.onBind(serviceIntent));
391 
392         Intent wrongIntent = new Intent("wrongIntent");
393         assertNull(service.onBind(wrongIntent));
394     }
395 
396     @Test
testMediaNotification()397     public void testMediaNotification() {
398         final int testId = 1001;
399         final String testChannelId = "channelId";
400         final Notification testNotification =
401                 new Notification.Builder(mContext, testChannelId).build();
402 
403         MediaSession2Service.MediaNotification notification =
404                 new MediaSession2Service.MediaNotification(testId, testNotification);
405         assertEquals(testId, notification.getNotificationId());
406         assertSame(testNotification, notification.getNotification());
407     }
408 
createConnectedController(Session2Token token)409     private MediaController2 createConnectedController(Session2Token token)
410             throws InterruptedException {
411         CountDownLatch latch = new CountDownLatch(1);
412         MediaController2 controller = new MediaController2.Builder(mContext, token)
413                 .setControllerCallback(sHandlerExecutor, new MediaController2.ControllerCallback() {
414                     @Override
415                     public void onConnected(MediaController2 controller,
416                             Session2CommandGroup allowedCommands) {
417                         latch.countDown();
418                         super.onConnected(controller, allowedCommands);
419                     }
420                 }).build();
421 
422         mControllers.add(controller);
423         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
424         return controller;
425     }
426 
createMediaSession2(String id)427     private MediaSession2 createMediaSession2(String id) {
428         return new MediaSession2.Builder(mContext)
429                 .setId(id)
430                 .setSessionCallback(sHandlerExecutor, new SessionCallback())
431                 .build();
432     }
433 
434     private static class SessionCallback extends MediaSession2.SessionCallback {
435         @Override
onConnect(MediaSession2 session, ControllerInfo controller)436         public Session2CommandGroup onConnect(MediaSession2 session,
437                 ControllerInfo controller) {
438             if (controller.getUid() == Process.myUid()) {
439                 return new Session2CommandGroup.Builder().build();
440             }
441             return null;
442         }
443     }
444 }
445