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 * <service android:name=".MyMediaBrowserService" 59 * android:label="@string/service_name" > 60 * <intent-filter> 61 * <action android:name="android.media.browse.MediaBrowserService" /> 62 * </intent-filter> 63 * </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 Result(Object debug)110 Result(Object debug) { 111 mDebug = debug; 112 } 113 114 /** 115 * Send the result back to the caller. 116 */ sendResult(T result)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 */ detach()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 isDone()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 */ onResultSent(T result)149 void onResultSent(T result) { 150 } 151 } 152 153 private class ServiceBinder extends IMediaBrowserService.Stub { 154 @Override connect(final String pkg, final Bundle rootHints, final IMediaBrowserServiceCallbacks callbacks)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 disconnect(final IMediaBrowserServiceCallbacks callbacks)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 addSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks)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 removeSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks)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 onCreate()267 public void onCreate() { 268 super.onCreate(); 269 mBinder = new ServiceBinder(); 270 } 271 272 @Override onBind(Intent intent)273 public IBinder onBind(Intent intent) { 274 if (SERVICE_INTERFACE.equals(intent.getAction())) { 275 return mBinder; 276 } 277 return null; 278 } 279 280 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)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 */ onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)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 */ onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)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 */ setSessionToken(final MediaSession.Token token)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 */ getSessionToken()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 */ notifyChildrenChanged(@onNull final String parentId)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 */ isValidPackage(String pkg, int uid)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 */ addSubscription(String id, ConnectionRecord connection)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 */ performLoadChildren(final String parentId, final ConnectionRecord connection)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 */ BrowserRoot(@onNull String rootId, @Nullable Bundle extras)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 */ getRootId()482 public String getRootId() { 483 return mRootId; 484 } 485 486 /** 487 * Gets any extras about the brwoser service. 488 */ getExtras()489 public Bundle getExtras() { 490 return mExtras; 491 } 492 } 493 } 494