1 /* 2 * Copyright 2018 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 androidx.media; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.Service; 22 import android.content.ComponentName; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.os.IBinder; 26 import android.support.v4.media.MediaBrowserCompat.MediaItem; 27 28 import androidx.annotation.CallSuper; 29 import androidx.annotation.GuardedBy; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.media.MediaBrowserServiceCompat.BrowserRoot; 33 import androidx.media.MediaSession2.ControllerInfo; 34 import androidx.media.SessionToken2.TokenType; 35 36 import java.util.List; 37 38 /** 39 * Base class for media session services, which is the service version of the {@link MediaSession2}. 40 * <p> 41 * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants 42 * to keep media playback in the background. 43 * <p> 44 * Here's the benefits of using {@link MediaSessionService2} instead of 45 * {@link MediaSession2}. 46 * <ul> 47 * <li>Another app can know that your app supports {@link MediaSession2} even when your app 48 * isn't running. 49 * <li>Another app can start playback of your app even when your app isn't running. 50 * </ul> 51 * For example, user's voice command can start playback of your app even when it's not running. 52 * <p> 53 * To extend this class, adding followings directly to your {@code AndroidManifest.xml}. 54 * <pre> 55 * <service android:name="component_name_of_your_implementation" > 56 * <intent-filter> 57 * <action android:name="android.media.MediaSessionService2" /> 58 * </intent-filter> 59 * </service></pre> 60 * <p> 61 * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't 62 * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By 63 * default, an empty string will be used for ID of the service. If you want to specify an ID, 64 * declare metadata in the manifest as follows. 65 * <pre> 66 * <service android:name="component_name_of_your_implementation" > 67 * <intent-filter> 68 * <action android:name="android.media.MediaSessionService2" /> 69 * </intent-filter> 70 * <meta-data android:name="android.media.session" 71 * android:value="session_id"/> 72 * </service></pre> 73 * <p> 74 * It's recommended for an app to have a single {@link MediaSessionService2} declared in the 75 * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another 76 * app fails to pick the right session service when it wants to start the playback this app. 77 * <p> 78 * If there's conflicts with the session ID among the services, services wouldn't be available for 79 * any controllers. 80 * <p> 81 * Topic covered here: 82 * <ol> 83 * <li><a href="#ServiceLifecycle">Service Lifecycle</a> 84 * <li><a href="#Permissions">Permissions</a> 85 * </ol> 86 * <div class="special reference"> 87 * <a name="ServiceLifecycle"></a> 88 * <h3>Service Lifecycle</h3> 89 * <p> 90 * Session service is bounded service. When a {@link MediaController2} is created for the 91 * session service, the controller binds to the session service. {@link #onCreateSession(String)} 92 * may be called after the {@link #onCreate} if the service hasn't created yet. 93 * <p> 94 * After the binding, session's 95 * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} 96 * will be called to accept or reject connection request from a controller. If the connection is 97 * rejected, the controller will unbind. If it's accepted, the controller will be available to use 98 * and keep binding. 99 * <p> 100 * When playback is started for this session service, {@link #onUpdateNotification()} 101 * is called and service would become a foreground service. It's needed to keep playback after the 102 * controller is destroyed. The session service becomes background service when the playback is 103 * stopped. 104 * <p> 105 * The service is destroyed when the session is closed, or no media controller is bounded to the 106 * session while the service is not running as a foreground service. 107 * <a name="Permissions"></a> 108 * <h3>Permissions</h3> 109 * <p> 110 * Any app can bind to the session service with controller, but the controller can be used only if 111 * the session service accepted the connection request through 112 * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}. 113 */ 114 public abstract class MediaSessionService2 extends Service { 115 /** 116 * This is the interface name that a service implementing a session service should say that it 117 * support -- that is, this is the action it uses for its intent filter. 118 */ 119 public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2"; 120 121 /** 122 * Name under which a MediaSessionService2 component publishes information about itself. 123 * This meta-data must provide a string value for the ID. 124 */ 125 public static final String SERVICE_META_DATA = "android.media.session"; 126 127 // Stub BrowserRoot for accepting any connction here. 128 // See MyBrowserService#onGetRoot() for detail. 129 static final BrowserRoot sDefaultBrowserRoot = new BrowserRoot(SERVICE_INTERFACE, null); 130 131 private final MediaBrowserServiceCompat mBrowserServiceCompat; 132 133 private final Object mLock = new Object(); 134 @GuardedBy("mLock") 135 private NotificationManager mNotificationManager; 136 @GuardedBy("mLock") 137 private Intent mStartSelfIntent; 138 @GuardedBy("mLock") 139 private boolean mIsRunningForeground; 140 @GuardedBy("mLock") 141 private MediaSession2 mSession; 142 143 public MediaSessionService2() { 144 super(); 145 mBrowserServiceCompat = createBrowserServiceCompat(); 146 } 147 148 MediaBrowserServiceCompat createBrowserServiceCompat() { 149 return new MyBrowserService(); 150 } 151 152 /** 153 * Default implementation for {@link MediaSessionService2} to initialize session service. 154 * <p> 155 * Override this method if you need your own initialization. Derived classes MUST call through 156 * to the super class's implementation of this method. 157 */ 158 @CallSuper 159 @Override 160 public void onCreate() { 161 super.onCreate(); 162 mBrowserServiceCompat.attachToBaseContext(this); 163 mBrowserServiceCompat.onCreate(); 164 SessionToken2 token = new SessionToken2(this, 165 new ComponentName(getPackageName(), getClass().getName())); 166 if (token.getType() != getSessionType()) { 167 throw new RuntimeException("Expected session type " + getSessionType() 168 + " but was " + token.getType()); 169 } 170 MediaSession2 session = onCreateSession(token.getId()); 171 synchronized (mLock) { 172 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 173 mStartSelfIntent = new Intent(this, getClass()); 174 mSession = session; 175 if (mSession == null || !token.getId().equals(mSession.getToken().getId())) { 176 throw new RuntimeException("Expected session with id " + token.getId() 177 + ", but got " + mSession); 178 } 179 mBrowserServiceCompat.setSessionToken(mSession.getToken().getSessionCompatToken()); 180 } 181 } 182 183 @TokenType int getSessionType() { 184 return SessionToken2.TYPE_SESSION_SERVICE; 185 } 186 187 /** 188 * Called when another app requested to start this service to get {@link MediaSession2}. 189 * <p> 190 * Session service will accept or reject the connection with the 191 * {@link MediaSession2.SessionCallback} in the created session. 192 * <p> 193 * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the 194 * expected ID that you've specified through the AndroidManifest.xml. 195 * <p> 196 * This method will be called on the main thread. 197 * 198 * @param sessionId session id written in the AndroidManifest.xml. 199 * @return a new session 200 * @see MediaSession2.Builder 201 * @see #getSession() 202 */ 203 public @NonNull abstract MediaSession2 onCreateSession(String sessionId); 204 205 /** 206 * Called when the playback state of this session is changed so notification needs update. 207 * Override this method to show or cancel your own notification UI. 208 * <p> 209 * With the notification returned here, the service become foreground service when the playback 210 * is started. It becomes background service after the playback is stopped. 211 * 212 * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown. 213 */ 214 public @Nullable MediaNotification onUpdateNotification() { 215 return null; 216 } 217 218 /** 219 * Get instance of the {@link MediaSession2} that you've previously created with the 220 * {@link #onCreateSession} for this service. 221 * <p> 222 * This may be {@code null} before the {@link #onCreate()} is finished. 223 * 224 * @return created session 225 */ 226 public final @Nullable MediaSession2 getSession() { 227 synchronized (mLock) { 228 return mSession; 229 } 230 } 231 232 /** 233 * Default implementation for {@link MediaSessionService2} to handle incoming binding 234 * request. If the request is for getting the session, the intent will have action 235 * {@link #SERVICE_INTERFACE}. 236 * <p> 237 * Override this method if this service also needs to handle binder requests other than 238 * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's 239 * implementation of this method. 240 * 241 * @param intent 242 * @return Binder 243 */ 244 @CallSuper 245 @Nullable 246 @Override 247 public IBinder onBind(Intent intent) { 248 if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction()) 249 || MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) { 250 // Change the intent action for browser service. 251 Intent browserServiceIntent = new Intent(intent); 252 browserServiceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE); 253 return mBrowserServiceCompat.onBind(intent); 254 } 255 return null; 256 } 257 258 MediaBrowserServiceCompat getServiceCompat() { 259 return mBrowserServiceCompat; 260 } 261 262 /** 263 * Returned by {@link #onUpdateNotification()} for making session service foreground service 264 * to keep playback running in the background. It's highly recommended to show media style 265 * notification here. 266 */ 267 public static class MediaNotification { 268 private final int mNotificationId; 269 private final Notification mNotification; 270 271 /** 272 * Default constructor 273 * 274 * @param notificationId notification id to be used for 275 * {@link NotificationManager#notify(int, Notification)}. 276 * @param notification a notification to make session service foreground service. Media 277 * style notification is recommended here. 278 */ 279 public MediaNotification(int notificationId, @NonNull Notification notification) { 280 if (notification == null) { 281 throw new IllegalArgumentException("notification shouldn't be null"); 282 } 283 mNotificationId = notificationId; 284 mNotification = notification; 285 } 286 287 /** 288 * Gets the id of the id. 289 * 290 * @return the notification id 291 */ 292 public int getNotificationId() { 293 return mNotificationId; 294 } 295 296 /** 297 * Gets the notification. 298 * 299 * @return the notification 300 */ 301 public @NonNull Notification getNotification() { 302 return mNotification; 303 } 304 } 305 306 private static class MyBrowserService extends MediaBrowserServiceCompat { 307 @Override 308 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { 309 // Returns *stub* root here. Here's the reason. 310 // 1. A non-null BrowserRoot should be returned here to keep the binding 311 // 2. MediaSessionService2 is defined as the simplified version of the library 312 // service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat 313 // specific operations. 314 return sDefaultBrowserRoot; 315 } 316 317 @Override 318 public void onLoadChildren(String parentId, Result<List<MediaItem>> result) { 319 // Disallow loading children. 320 } 321 } 322 } 323