Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2014 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.service.media;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.annotation.SdkConstant;
     23 import android.annotation.SdkConstant.SdkConstantType;
     24 import android.app.Service;
     25 import android.content.Intent;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.ParceledListSlice;
     28 import android.media.browse.MediaBrowser;
     29 import android.media.session.MediaSession;
     30 import android.os.Binder;
     31 import android.os.Bundle;
     32 import android.os.IBinder;
     33 import android.os.Handler;
     34 import android.os.RemoteException;
     35 import android.service.media.IMediaBrowserService;
     36 import android.service.media.IMediaBrowserServiceCallbacks;
     37 import android.util.ArrayMap;
     38 import android.util.Log;
     39 
     40 import java.io.FileDescriptor;
     41 import java.io.PrintWriter;
     42 import java.util.HashSet;
     43 import java.util.List;
     44 
     45 /**
     46  * Base class for media browse services.
     47  * <p>
     48  * Media browse services enable applications to browse media content provided by an application
     49  * and ask the application to start playing it.  They may also be used to control content that
     50  * is already playing by way of a {@link MediaSession}.
     51  * </p>
     52  *
     53  * To extend this class, you must declare the service in your manifest file with
     54  * an intent filter with the {@link #SERVICE_INTERFACE} action.
     55  *
     56  * For example:
     57  * </p><pre>
     58  * &lt;service android:name=".MyMediaBrowserService"
     59  *          android:label="&#64;string/service_name" >
     60  *     &lt;intent-filter>
     61  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
     62  *     &lt;/intent-filter>
     63  * &lt;/service>
     64  * </pre>
     65  *
     66  */
     67 public abstract class MediaBrowserService extends Service {
     68     private static final String TAG = "MediaBrowserService";
     69     private static final boolean DBG = false;
     70 
     71     /**
     72      * The {@link Intent} that must be declared as handled by the service.
     73      */
     74     @SdkConstant(SdkConstantType.SERVICE_ACTION)
     75     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
     76 
     77     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
     78     private final Handler mHandler = new Handler();
     79     private ServiceBinder mBinder;
     80     MediaSession.Token mSession;
     81 
     82     /**
     83      * All the info about a connection.
     84      */
     85     private class ConnectionRecord {
     86         String pkg;
     87         Bundle rootHints;
     88         IMediaBrowserServiceCallbacks callbacks;
     89         BrowserRoot root;
     90         HashSet<String> subscriptions = new HashSet();
     91     }
     92 
     93     /**
     94      * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
     95      * <p>
     96      * Each of the methods that takes one of these to send the result must call
     97      * {@link #sendResult} to respond to the caller with the given results.  If those
     98      * functions return without calling {@link #sendResult}, they must instead call
     99      * {@link #detach} before returning, and then may call {@link #sendResult} when
    100      * they are done.  If more than one of those methods is called, an exception will
    101      * be thrown.
    102      *
    103      * @see MediaBrowserService#onLoadChildren
    104      */
    105     public class Result<T> {
    106         private Object mDebug;
    107         private boolean mDetachCalled;
    108         private boolean mSendResultCalled;
    109 
    110         Result(Object debug) {
    111             mDebug = debug;
    112         }
    113 
    114         /**
    115          * Send the result back to the caller.
    116          */
    117         public void sendResult(T result) {
    118             if (mSendResultCalled) {
    119                 throw new IllegalStateException("sendResult() called twice for: " + mDebug);
    120             }
    121             mSendResultCalled = true;
    122             onResultSent(result);
    123         }
    124 
    125         /**
    126          * Detach this message from the current thread and allow the {@link #sendResult}
    127          * call to happen later.
    128          */
    129         public void detach() {
    130             if (mDetachCalled) {
    131                 throw new IllegalStateException("detach() called when detach() had already"
    132                         + " been called for: " + mDebug);
    133             }
    134             if (mSendResultCalled) {
    135                 throw new IllegalStateException("detach() called when sendResult() had already"
    136                         + " been called for: " + mDebug);
    137             }
    138             mDetachCalled = true;
    139         }
    140 
    141         boolean isDone() {
    142             return mDetachCalled || mSendResultCalled;
    143         }
    144 
    145         /**
    146          * Called when the result is sent, after assertions about not being called twice
    147          * have happened.
    148          */
    149         void onResultSent(T result) {
    150         }
    151     }
    152 
    153     private class ServiceBinder extends IMediaBrowserService.Stub {
    154         @Override
    155         public void connect(final String pkg, final Bundle rootHints,
    156                 final IMediaBrowserServiceCallbacks callbacks) {
    157 
    158             final int uid = Binder.getCallingUid();
    159             if (!isValidPackage(pkg, uid)) {
    160                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
    161                         + " package=" + pkg);
    162             }
    163 
    164             mHandler.post(new Runnable() {
    165                     @Override
    166                     public void run() {
    167                         final IBinder b = callbacks.asBinder();
    168 
    169                         // Clear out the old subscriptions.  We are getting new ones.
    170                         mConnections.remove(b);
    171 
    172                         final ConnectionRecord connection = new ConnectionRecord();
    173                         connection.pkg = pkg;
    174                         connection.rootHints = rootHints;
    175                         connection.callbacks = callbacks;
    176 
    177                         connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
    178 
    179                         // If they didn't return something, don't allow this client.
    180                         if (connection.root == null) {
    181                             Log.i(TAG, "No root for client " + pkg + " from service "
    182                                     + getClass().getName());
    183                             try {
    184                                 callbacks.onConnectFailed();
    185                             } catch (RemoteException ex) {
    186                                 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
    187                                         + "pkg=" + pkg);
    188                             }
    189                         } else {
    190                             try {
    191                                 mConnections.put(b, connection);
    192                                 if (mSession != null) {
    193                                     callbacks.onConnect(connection.root.getRootId(),
    194                                             mSession, connection.root.getExtras());
    195                                 }
    196                             } catch (RemoteException ex) {
    197                                 Log.w(TAG, "Calling onConnect() failed. Dropping client. "
    198                                         + "pkg=" + pkg);
    199                                 mConnections.remove(b);
    200                             }
    201                         }
    202                     }
    203                 });
    204         }
    205 
    206         @Override
    207         public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
    208             mHandler.post(new Runnable() {
    209                     @Override
    210                     public void run() {
    211                         final IBinder b = callbacks.asBinder();
    212 
    213                         // Clear out the old subscriptions.  We are getting new ones.
    214                         final ConnectionRecord old = mConnections.remove(b);
    215                         if (old != null) {
    216                             // TODO
    217                         }
    218                     }
    219                 });
    220         }
    221 
    222 
    223         @Override
    224         public void addSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks) {
    225             mHandler.post(new Runnable() {
    226                     @Override
    227                     public void run() {
    228                         final IBinder b = callbacks.asBinder();
    229 
    230                         // Get the record for the connection
    231                         final ConnectionRecord connection = mConnections.get(b);
    232                         if (connection == null) {
    233                             Log.w(TAG, "addSubscription for callback that isn't registered id="
    234                                 + id);
    235                             return;
    236                         }
    237 
    238                         MediaBrowserService.this.addSubscription(id, connection);
    239                     }
    240                 });
    241         }
    242 
    243         @Override
    244         public void removeSubscription(final String id,
    245                 final IMediaBrowserServiceCallbacks callbacks) {
    246             mHandler.post(new Runnable() {
    247                 @Override
    248                 public void run() {
    249                     final IBinder b = callbacks.asBinder();
    250 
    251                     ConnectionRecord connection = mConnections.get(b);
    252                     if (connection == null) {
    253                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
    254                                 + id);
    255                         return;
    256                     }
    257                     if (!connection.subscriptions.remove(id)) {
    258                         Log.w(TAG, "removeSubscription called for " + id
    259                                 + " which is not subscribed");
    260                     }
    261                 }
    262             });
    263         }
    264     }
    265 
    266     @Override
    267     public void onCreate() {
    268         super.onCreate();
    269         mBinder = new ServiceBinder();
    270     }
    271 
    272     @Override
    273     public IBinder onBind(Intent intent) {
    274         if (SERVICE_INTERFACE.equals(intent.getAction())) {
    275             return mBinder;
    276         }
    277         return null;
    278     }
    279 
    280     @Override
    281     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
    282     }
    283 
    284     /**
    285      * Called to get the root information for browsing by a particular client.
    286      * <p>
    287      * The implementation should verify that the client package has
    288      * permission to access browse media information before returning
    289      * the root id; it should return null if the client is not
    290      * allowed to access this information.
    291      * </p>
    292      *
    293      * @param clientPackageName The package name of the application
    294      * which is requesting access to browse media.
    295      * @param clientUid The uid of the application which is requesting
    296      * access to browse media.
    297      * @param rootHints An optional bundle of service-specific arguments to send
    298      * to the media browse service when connecting and retrieving the root id
    299      * for browsing, or null if none.  The contents of this bundle may affect
    300      * the information returned when browsing.
    301      */
    302     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
    303             int clientUid, @Nullable Bundle rootHints);
    304 
    305     /**
    306      * Called to get information about the children of a media item.
    307      * <p>
    308      * Implementations must call result.{@link Result#sendResult result.sendResult} with the list
    309      * of children. If loading the children will be an expensive operation that should be performed
    310      * on another thread, result.{@link Result#detach result.detach} may be called before returning
    311      * from this function, and then {@link Result#sendResult result.sendResult} called when
    312      * the loading is complete.
    313      *
    314      * @param parentId The id of the parent media item whose
    315      * children are to be queried.
    316      * @return The list of children, or null if the id is invalid.
    317      */
    318     public abstract void onLoadChildren(@NonNull String parentId,
    319             @NonNull Result<List<MediaBrowser.MediaItem>> result);
    320 
    321     /**
    322      * Call to set the media session.
    323      * <p>
    324      * This should be called as soon as possible during the service's startup.
    325      * It may only be called once.
    326      */
    327     public void setSessionToken(final MediaSession.Token token) {
    328         if (token == null) {
    329             throw new IllegalArgumentException("Session token may not be null.");
    330         }
    331         if (mSession != null) {
    332             throw new IllegalStateException("The session token has already been set.");
    333         }
    334         mSession = token;
    335         mHandler.post(new Runnable() {
    336             @Override
    337             public void run() {
    338                 for (IBinder key : mConnections.keySet()) {
    339                     ConnectionRecord connection = mConnections.get(key);
    340                     try {
    341                         connection.callbacks.onConnect(connection.root.getRootId(), token,
    342                                 connection.root.getExtras());
    343                     } catch (RemoteException e) {
    344                         Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
    345                         mConnections.remove(key);
    346                     }
    347                 }
    348             }
    349         });
    350     }
    351 
    352     /**
    353      * Gets the session token, or null if it has not yet been created
    354      * or if it has been destroyed.
    355      */
    356     public @Nullable MediaSession.Token getSessionToken() {
    357         return mSession;
    358     }
    359 
    360     /**
    361      * Notifies all connected media browsers that the children of
    362      * the specified parent id have changed in some way.
    363      * This will cause browsers to fetch subscribed content again.
    364      *
    365      * @param parentId The id of the parent media item whose
    366      * children changed.
    367      */
    368     public void notifyChildrenChanged(@NonNull final String parentId) {
    369         if (parentId == null) {
    370             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
    371         }
    372         mHandler.post(new Runnable() {
    373             @Override
    374             public void run() {
    375                 for (IBinder binder : mConnections.keySet()) {
    376                     ConnectionRecord connection = mConnections.get(binder);
    377                     if (connection.subscriptions.contains(parentId)) {
    378                         performLoadChildren(parentId, connection);
    379                     }
    380                 }
    381             }
    382         });
    383     }
    384 
    385     /**
    386      * Return whether the given package is one of the ones that is owned by the uid.
    387      */
    388     private boolean isValidPackage(String pkg, int uid) {
    389         if (pkg == null) {
    390             return false;
    391         }
    392         final PackageManager pm = getPackageManager();
    393         final String[] packages = pm.getPackagesForUid(uid);
    394         final int N = packages.length;
    395         for (int i=0; i<N; i++) {
    396             if (packages[i].equals(pkg)) {
    397                 return true;
    398             }
    399         }
    400         return false;
    401     }
    402 
    403     /**
    404      * Save the subscription and if it is a new subscription send the results.
    405      */
    406     private void addSubscription(String id, ConnectionRecord connection) {
    407         // Save the subscription
    408         final boolean added = connection.subscriptions.add(id);
    409 
    410         // If this is a new subscription, send the results
    411         if (added) {
    412             performLoadChildren(id, connection);
    413         }
    414     }
    415 
    416     /**
    417      * Call onLoadChildren and then send the results back to the connection.
    418      * <p>
    419      * Callers must make sure that this connection is still connected.
    420      */
    421     private void performLoadChildren(final String parentId, final ConnectionRecord connection) {
    422         final Result<List<MediaBrowser.MediaItem>> result
    423                 = new Result<List<MediaBrowser.MediaItem>>(parentId) {
    424             @Override
    425             void onResultSent(List<MediaBrowser.MediaItem> list) {
    426                 if (list == null) {
    427                     throw new IllegalStateException("onLoadChildren sent null list for id "
    428                             + parentId);
    429                 }
    430                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
    431                     if (DBG) {
    432                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
    433                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
    434                     }
    435                     return;
    436                 }
    437 
    438                 final ParceledListSlice<MediaBrowser.MediaItem> pls = new ParceledListSlice(list);
    439                 try {
    440                     connection.callbacks.onLoadChildren(parentId, pls);
    441                 } catch (RemoteException ex) {
    442                     // The other side is in the process of crashing.
    443                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
    444                             + " package=" + connection.pkg);
    445                 }
    446             }
    447         };
    448 
    449         onLoadChildren(parentId, result);
    450 
    451         if (!result.isDone()) {
    452             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
    453                     + " before returning for package=" + connection.pkg + " id=" + parentId);
    454         }
    455     }
    456 
    457     /**
    458      * Contains information that the browser service needs to send to the client
    459      * when first connected.
    460      */
    461     public static final class BrowserRoot {
    462         final private String mRootId;
    463         final private Bundle mExtras;
    464 
    465         /**
    466          * Constructs a browser root.
    467          * @param rootId The root id for browsing.
    468          * @param extras Any extras about the browser service.
    469          */
    470         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
    471             if (rootId == null) {
    472                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
    473                         "Use null for BrowserRoot instead.");
    474             }
    475             mRootId = rootId;
    476             mExtras = extras;
    477         }
    478 
    479         /**
    480          * Gets the root id for browsing.
    481          */
    482         public String getRootId() {
    483             return mRootId;
    484         }
    485 
    486         /**
    487          * Gets any extras about the brwoser service.
    488          */
    489         public Bundle getExtras() {
    490             return mExtras;
    491         }
    492     }
    493 }
    494