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;
18 
19 import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
20 import static android.media.MediaConstants.KEY_PACKAGE_NAME;
21 import static android.media.MediaConstants.KEY_PID;
22 
23 import android.annotation.CallSuper;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.Notification;
27 import android.app.NotificationManager;
28 import android.app.Service;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.media.MediaSession2.ControllerInfo;
32 import android.media.session.MediaSessionManager;
33 import android.media.session.MediaSessionManager.RemoteUserInfo;
34 import android.os.Binder;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.IBinder;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 
41 import java.lang.ref.WeakReference;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * This API is not generally intended for third party application developers.
48  * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
49  * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
50  * Library</a> for consistent behavior across all devices.
51  * <p>
52  * Service containing {@link MediaSession2}.
53  */
54 public abstract class MediaSession2Service extends Service {
55     /**
56      * The {@link Intent} that must be declared as handled by the service.
57      */
58     public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
59 
60     private static final String TAG = "MediaSession2Service";
61     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
62 
63     private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback =
64             new MediaSession2.ForegroundServiceEventCallback() {
65                 @Override
66                 public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
67                     MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive);
68                 }
69 
70                 @Override
71                 public void onSessionClosed(MediaSession2 session) {
72                     removeSession(session);
73                 }
74             };
75 
76     private final Object mLock = new Object();
77     //@GuardedBy("mLock")
78     private NotificationManager mNotificationManager;
79     //@GuardedBy("mLock")
80     private MediaSessionManager mMediaSessionManager;
81     //@GuardedBy("mLock")
82     private Intent mStartSelfIntent;
83     //@GuardedBy("mLock")
84     private Map<String, MediaSession2> mSessions = new ArrayMap<>();
85     //@GuardedBy("mLock")
86     private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>();
87     //@GuardedBy("mLock")
88     private MediaSession2ServiceStub mStub;
89 
90     /**
91      * Called by the system when the service is first created. Do not call this method directly.
92      * <p>
93      * Override this method if you need your own initialization. Derived classes MUST call through
94      * to the super class's implementation of this method.
95      */
96     @CallSuper
97     @Override
onCreate()98     public void onCreate() {
99         super.onCreate();
100         synchronized (mLock) {
101             mStub = new MediaSession2ServiceStub(this);
102             mStartSelfIntent = new Intent(this, this.getClass());
103             mNotificationManager =
104                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
105             mMediaSessionManager =
106                     (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
107         }
108     }
109 
110     @CallSuper
111     @Override
112     @Nullable
onBind(@onNull Intent intent)113     public IBinder onBind(@NonNull Intent intent) {
114         if (SERVICE_INTERFACE.equals(intent.getAction())) {
115             synchronized (mLock) {
116                 return mStub;
117             }
118         }
119         return null;
120     }
121 
122     /**
123      * Called by the system to notify that it is no longer used and is being removed. Do not call
124      * this method directly.
125      * <p>
126      * Override this method if you need your own clean up. Derived classes MUST call through
127      * to the super class's implementation of this method.
128      */
129     @CallSuper
130     @Override
onDestroy()131     public void onDestroy() {
132         super.onDestroy();
133         synchronized (mLock) {
134             List<MediaSession2> sessions = getSessions();
135             for (MediaSession2 session : sessions) {
136                 removeSession(session);
137             }
138             mSessions.clear();
139             mNotifications.clear();
140         }
141         mStub.close();
142     }
143 
144     /**
145      * Called when a {@link MediaController2} is created with the this service's
146      * {@link Session2Token}. Return the session for telling the controller which session to
147      * connect. Return {@code null} to reject the connection from this controller.
148      * <p>
149      * Session returned here will be added to this service automatically. You don't need to call
150      * {@link #addSession(MediaSession2)} for that.
151      * <p>
152      * This method is always called on the main thread.
153      *
154      * @param controllerInfo information of the controller which is trying to connect.
155      * @return a {@link MediaSession2} instance for the controller to connect to, or {@code null}
156      *         to reject connection
157      * @see MediaSession2.Builder
158      * @see #getSessions()
159      */
160     @Nullable
onGetSession(@onNull ControllerInfo controllerInfo)161     public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo);
162 
163     /**
164      * Called when notification UI needs update. Override this method to show or cancel your own
165      * notification UI.
166      * <p>
167      * This would be called on {@link MediaSession2}'s callback executor when playback state is
168      * changed.
169      * <p>
170      * With the notification returned here, the service becomes foreground service when the playback
171      * is started. Apps must request the permission
172      * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes
173      * background service after the playback is stopped.
174      *
175      * @param session a session that needs notification update.
176      * @return a {@link MediaNotification}. Can be {@code null}.
177      */
178     @Nullable
onUpdateNotification(@onNull MediaSession2 session)179     public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session);
180 
181     /**
182      * Adds a session to this service.
183      * <p>
184      * Added session will be removed automatically when it's closed, or removed when
185      * {@link #removeSession} is called.
186      *
187      * @param session a session to be added.
188      * @see #removeSession(MediaSession2)
189      */
addSession(@onNull MediaSession2 session)190     public final void addSession(@NonNull MediaSession2 session) {
191         if (session == null) {
192             throw new IllegalArgumentException("session shouldn't be null");
193         }
194         if (session.isClosed()) {
195             throw new IllegalArgumentException("session is already closed");
196         }
197         synchronized (mLock) {
198             MediaSession2 previousSession = mSessions.get(session.getId());
199             if (previousSession != null) {
200                 if (previousSession != session) {
201                     Log.w(TAG, "Session ID should be unique, ID=" + session.getId()
202                             + ", previous=" + previousSession + ", session=" + session);
203                 }
204                 return;
205             }
206             mSessions.put(session.getId(), session);
207             session.setForegroundServiceEventCallback(mForegroundServiceEventCallback);
208         }
209     }
210 
211     /**
212      * Removes a session from this service.
213      *
214      * @param session a session to be removed.
215      * @see #addSession(MediaSession2)
216      */
removeSession(@onNull MediaSession2 session)217     public final void removeSession(@NonNull MediaSession2 session) {
218         if (session == null) {
219             throw new IllegalArgumentException("session shouldn't be null");
220         }
221         MediaNotification notification;
222         synchronized (mLock) {
223             if (mSessions.get(session.getId()) != session) {
224                 // Session isn't added or removed already.
225                 return;
226             }
227             mSessions.remove(session.getId());
228             notification = mNotifications.remove(session);
229         }
230         session.setForegroundServiceEventCallback(null);
231         if (notification != null) {
232             mNotificationManager.cancel(notification.getNotificationId());
233         }
234         if (getSessions().isEmpty()) {
235             stopForeground(false);
236         }
237     }
238 
239     /**
240      * Gets the list of {@link MediaSession2}s that you've added to this service.
241      *
242      * @return sessions
243      */
getSessions()244     public final @NonNull List<MediaSession2> getSessions() {
245         List<MediaSession2> list = new ArrayList<>();
246         synchronized (mLock) {
247             list.addAll(mSessions.values());
248         }
249         return list;
250     }
251 
252     /**
253      * Returns the {@link MediaSessionManager}.
254      */
255     @NonNull
getMediaSessionManager()256     MediaSessionManager getMediaSessionManager() {
257         synchronized (mLock) {
258             return mMediaSessionManager;
259         }
260     }
261 
262     /**
263      * Called by registered {@link MediaSession2.ForegroundServiceEventCallback}
264      *
265      * @param session session with change
266      * @param playbackActive {@code true} if playback is active.
267      */
onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive)268     void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {
269         MediaNotification mediaNotification = onUpdateNotification(session);
270         if (mediaNotification == null) {
271             // The service implementation doesn't want to use the automatic start/stopForeground
272             // feature.
273             return;
274         }
275         synchronized (mLock) {
276             mNotifications.put(session, mediaNotification);
277         }
278         int id = mediaNotification.getNotificationId();
279         Notification notification = mediaNotification.getNotification();
280         if (!playbackActive) {
281             mNotificationManager.notify(id, notification);
282             return;
283         }
284         // playbackActive == true
285         startForegroundService(mStartSelfIntent);
286         startForeground(id, notification);
287     }
288 
289     /**
290      * This API is not generally intended for third party application developers.
291      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
292      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
293      * Library</a> for consistent behavior across all devices.
294      * <p>
295      * Returned by {@link #onUpdateNotification(MediaSession2)} for making session service
296      * foreground service to keep playback running in the background. It's highly recommended to
297      * show media style notification here.
298      */
299     public static class MediaNotification {
300         private final int mNotificationId;
301         private final Notification mNotification;
302 
303         /**
304          * Default constructor
305          *
306          * @param notificationId notification id to be used for
307          *        {@link NotificationManager#notify(int, Notification)}.
308          * @param notification a notification to make session service run in the foreground. Media
309          *        style notification is recommended here.
310          */
MediaNotification(int notificationId, @NonNull Notification notification)311         public MediaNotification(int notificationId, @NonNull Notification notification) {
312             if (notification == null) {
313                 throw new IllegalArgumentException("notification shouldn't be null");
314             }
315             mNotificationId = notificationId;
316             mNotification = notification;
317         }
318 
319         /**
320          * Gets the id of the notification.
321          *
322          * @return the notification id
323          */
getNotificationId()324         public int getNotificationId() {
325             return mNotificationId;
326         }
327 
328         /**
329          * Gets the notification.
330          *
331          * @return the notification
332          */
333         @NonNull
getNotification()334         public Notification getNotification() {
335             return mNotification;
336         }
337     }
338 
339     private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub
340             implements AutoCloseable {
341         final WeakReference<MediaSession2Service> mService;
342         final Handler mHandler;
343 
MediaSession2ServiceStub(MediaSession2Service service)344         MediaSession2ServiceStub(MediaSession2Service service) {
345             mService = new WeakReference<>(service);
346             mHandler = new Handler(service.getMainLooper());
347         }
348 
349         @Override
connect(Controller2Link caller, int seq, Bundle connectionRequest)350         public void connect(Controller2Link caller, int seq, Bundle connectionRequest) {
351             if (mService.get() == null) {
352                 if (DEBUG) {
353                     Log.d(TAG, "Service is already destroyed");
354                 }
355                 return;
356             }
357             if (caller == null || connectionRequest == null) {
358                 if (DEBUG) {
359                     Log.d(TAG, "Ignoring calls with illegal arguments, caller=" + caller
360                             + ", connectionRequest=" + connectionRequest);
361                 }
362                 return;
363             }
364             final int pid = Binder.getCallingPid();
365             final int uid = Binder.getCallingUid();
366             final long token = Binder.clearCallingIdentity();
367             try {
368                 mHandler.post(() -> {
369                     boolean shouldNotifyDisconnected = true;
370                     try {
371                         final MediaSession2Service service = mService.get();
372                         if (service == null) {
373                             if (DEBUG) {
374                                 Log.d(TAG, "Service isn't available");
375                             }
376                             return;
377                         }
378 
379                         String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
380                         // The Binder.getCallingPid() can be 0 for an oneway call from the
381                         // remote process. If it's the case, use PID from the connectionRequest.
382                         RemoteUserInfo remoteUserInfo = new RemoteUserInfo(
383                                 callingPkg,
384                                 pid == 0 ? connectionRequest.getInt(KEY_PID) : pid,
385                                 uid);
386 
387                         Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
388                         if (connectionHints == null) {
389                             Log.w(TAG, "connectionHints shouldn't be null.");
390                             connectionHints = Bundle.EMPTY;
391                         } else if (MediaSession2.hasCustomParcelable(connectionHints)) {
392                             Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
393                             connectionHints = Bundle.EMPTY;
394                         }
395 
396                         final ControllerInfo controllerInfo = new ControllerInfo(
397                                 remoteUserInfo,
398                                 service.getMediaSessionManager()
399                                         .isTrustedForMediaControl(remoteUserInfo),
400                                 caller,
401                                 connectionHints);
402 
403                         if (DEBUG) {
404                             Log.d(TAG, "Handling incoming connection request from the"
405                                     + " controller=" + controllerInfo);
406                         }
407 
408                         final MediaSession2 session;
409                         session = service.onGetSession(controllerInfo);
410 
411                         if (session == null) {
412                             if (DEBUG) {
413                                 Log.d(TAG, "Rejecting incoming connection request from the"
414                                         + " controller=" + controllerInfo);
415                             }
416                             // Note: Trusted controllers also can be rejected according to the
417                             // service implementation.
418                             return;
419                         }
420                         service.addSession(session);
421                         shouldNotifyDisconnected = false;
422                         session.onConnect(caller, pid, uid, seq, connectionRequest);
423                     } catch (Exception e) {
424                         // Don't propagate exception in service to the controller.
425                         Log.w(TAG, "Failed to add a session to session service", e);
426                     } finally {
427                         // Trick to call onDisconnected() in one place.
428                         if (shouldNotifyDisconnected) {
429                             if (DEBUG) {
430                                 Log.d(TAG, "Notifying the controller of its disconnection");
431                             }
432                             try {
433                                 caller.notifyDisconnected(0);
434                             } catch (RuntimeException e) {
435                                 // Controller may be died prematurely.
436                                 // Not an issue because we'll ignore it anyway.
437                             }
438                         }
439                     }
440                 });
441             } finally {
442                 Binder.restoreCallingIdentity(token);
443             }
444         }
445 
446         @Override
close()447         public void close() {
448             mHandler.removeCallbacksAndMessages(null);
449             mService.clear();
450         }
451     }
452 }
453