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