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