Home | History | Annotate | Download | only in media
      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  * &lt;service android:name="component_name_of_your_implementation" &gt;
     56  *   &lt;intent-filter&gt;
     57  *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
     58  *   &lt;/intent-filter&gt;
     59  * &lt;/service&gt;</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  * &lt;service android:name="component_name_of_your_implementation" &gt;
     67  *   &lt;intent-filter&gt;
     68  *     &lt;action android:name="android.media.MediaSessionService2" /&gt;
     69  *   &lt;/intent-filter&gt;
     70  *   &lt;meta-data android:name="android.media.session"
     71  *       android:value="session_id"/&gt;
     72  * &lt;/service&gt;</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