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 com.android.server.media.projection; 18 19 import com.android.server.Watchdog; 20 21 import android.Manifest; 22 import android.app.AppOpsManager; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.hardware.display.DisplayManager; 26 import android.media.MediaRouter; 27 import android.media.projection.IMediaProjectionManager; 28 import android.media.projection.IMediaProjection; 29 import android.media.projection.IMediaProjectionCallback; 30 import android.media.projection.IMediaProjectionWatcherCallback; 31 import android.media.projection.MediaProjectionInfo; 32 import android.media.projection.MediaProjectionManager; 33 import android.os.Binder; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.os.RemoteException; 38 import android.os.UserHandle; 39 import android.util.ArrayMap; 40 import android.util.Slog; 41 42 import com.android.internal.util.DumpUtils; 43 import com.android.server.SystemService; 44 45 import java.io.FileDescriptor; 46 import java.io.PrintWriter; 47 import java.util.Map; 48 49 /** 50 * Manages MediaProjection sessions. 51 * 52 * The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections, 53 * as well as the capabilities they grant. Any service using MediaProjection tokens as permission 54 * grants <b>must</b> validate the token before use by calling {@link 55 * IMediaProjectionService#isValidMediaProjection}. 56 */ 57 public final class MediaProjectionManagerService extends SystemService 58 implements Watchdog.Monitor { 59 private static final String TAG = "MediaProjectionManagerService"; 60 61 private final Object mLock = new Object(); // Protects the list of media projections 62 private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters; 63 private final CallbackDelegate mCallbackDelegate; 64 65 private final Context mContext; 66 private final AppOpsManager mAppOps; 67 68 private final MediaRouter mMediaRouter; 69 private final MediaRouterCallback mMediaRouterCallback; 70 private MediaRouter.RouteInfo mMediaRouteInfo; 71 72 private IBinder mProjectionToken; 73 private MediaProjection mProjectionGrant; 74 75 public MediaProjectionManagerService(Context context) { 76 super(context); 77 mContext = context; 78 mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>(); 79 mCallbackDelegate = new CallbackDelegate(); 80 mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); 81 mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); 82 mMediaRouterCallback = new MediaRouterCallback(); 83 Watchdog.getInstance().addMonitor(this); 84 } 85 86 @Override 87 public void onStart() { 88 publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(), 89 false /*allowIsolated*/); 90 mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, 91 MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); 92 } 93 94 @Override 95 public void onSwitchUser(int userId) { 96 mMediaRouter.rebindAsUser(userId); 97 synchronized (mLock) { 98 if (mProjectionGrant != null) { 99 mProjectionGrant.stop(); 100 } 101 } 102 } 103 104 @Override 105 public void monitor() { 106 synchronized (mLock) { /* check for deadlock */ } 107 } 108 109 private void startProjectionLocked(final MediaProjection projection) { 110 if (mProjectionGrant != null) { 111 mProjectionGrant.stop(); 112 } 113 if (mMediaRouteInfo != null) { 114 mMediaRouter.getFallbackRoute().select(); 115 } 116 mProjectionToken = projection.asBinder(); 117 mProjectionGrant = projection; 118 dispatchStart(projection); 119 } 120 121 private void stopProjectionLocked(final MediaProjection projection) { 122 mProjectionToken = null; 123 mProjectionGrant = null; 124 dispatchStop(projection); 125 } 126 127 private void addCallback(final IMediaProjectionWatcherCallback callback) { 128 IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { 129 @Override 130 public void binderDied() { 131 removeCallback(callback); 132 } 133 }; 134 synchronized (mLock) { 135 mCallbackDelegate.add(callback); 136 linkDeathRecipientLocked(callback, deathRecipient); 137 } 138 } 139 140 private void removeCallback(IMediaProjectionWatcherCallback callback) { 141 synchronized (mLock) { 142 unlinkDeathRecipientLocked(callback); 143 mCallbackDelegate.remove(callback); 144 } 145 } 146 147 private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback, 148 IBinder.DeathRecipient deathRecipient) { 149 try { 150 final IBinder token = callback.asBinder(); 151 token.linkToDeath(deathRecipient, 0); 152 mDeathEaters.put(token, deathRecipient); 153 } catch (RemoteException e) { 154 Slog.e(TAG, "Unable to link to death for media projection monitoring callback", e); 155 } 156 } 157 158 private void unlinkDeathRecipientLocked(IMediaProjectionWatcherCallback callback) { 159 final IBinder token = callback.asBinder(); 160 IBinder.DeathRecipient deathRecipient = mDeathEaters.remove(token); 161 if (deathRecipient != null) { 162 token.unlinkToDeath(deathRecipient, 0); 163 } 164 } 165 166 private void dispatchStart(MediaProjection projection) { 167 mCallbackDelegate.dispatchStart(projection); 168 } 169 170 private void dispatchStop(MediaProjection projection) { 171 mCallbackDelegate.dispatchStop(projection); 172 } 173 174 private boolean isValidMediaProjection(IBinder token) { 175 synchronized (mLock) { 176 if (mProjectionToken != null) { 177 return mProjectionToken.equals(token); 178 } 179 return false; 180 } 181 } 182 183 private MediaProjectionInfo getActiveProjectionInfo() { 184 synchronized (mLock) { 185 if (mProjectionGrant == null) { 186 return null; 187 } 188 return mProjectionGrant.getProjectionInfo(); 189 } 190 } 191 192 private void dump(final PrintWriter pw) { 193 pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)"); 194 synchronized (mLock) { 195 pw.println("Media Projection: "); 196 if (mProjectionGrant != null ) { 197 mProjectionGrant.dump(pw); 198 } else { 199 pw.println("null"); 200 } 201 } 202 } 203 204 private final class BinderService extends IMediaProjectionManager.Stub { 205 206 @Override // Binder call 207 public boolean hasProjectionPermission(int uid, String packageName) { 208 long token = Binder.clearCallingIdentity(); 209 boolean hasPermission = false; 210 try { 211 hasPermission |= checkPermission(packageName, 212 android.Manifest.permission.CAPTURE_VIDEO_OUTPUT) 213 || mAppOps.noteOpNoThrow( 214 AppOpsManager.OP_PROJECT_MEDIA, uid, packageName) 215 == AppOpsManager.MODE_ALLOWED; 216 } finally { 217 Binder.restoreCallingIdentity(token); 218 } 219 return hasPermission; 220 } 221 222 @Override // Binder call 223 public IMediaProjection createProjection(int uid, String packageName, int type, 224 boolean isPermanentGrant) { 225 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) 226 != PackageManager.PERMISSION_GRANTED) { 227 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant " 228 + "projection permission"); 229 } 230 if (packageName == null || packageName.isEmpty()) { 231 throw new IllegalArgumentException("package name must not be empty"); 232 } 233 long callingToken = Binder.clearCallingIdentity(); 234 MediaProjection projection; 235 try { 236 projection = new MediaProjection(type, uid, packageName); 237 if (isPermanentGrant) { 238 mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, 239 projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); 240 } 241 } finally { 242 Binder.restoreCallingIdentity(callingToken); 243 } 244 return projection; 245 } 246 247 @Override // Binder call 248 public boolean isValidMediaProjection(IMediaProjection projection) { 249 return MediaProjectionManagerService.this.isValidMediaProjection( 250 projection.asBinder()); 251 } 252 253 @Override // Binder call 254 public MediaProjectionInfo getActiveProjectionInfo() { 255 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) 256 != PackageManager.PERMISSION_GRANTED) { 257 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " 258 + "projection callbacks"); 259 } 260 final long token = Binder.clearCallingIdentity(); 261 try { 262 return MediaProjectionManagerService.this.getActiveProjectionInfo(); 263 } finally { 264 Binder.restoreCallingIdentity(token); 265 } 266 } 267 268 @Override // Binder call 269 public void stopActiveProjection() { 270 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) 271 != PackageManager.PERMISSION_GRANTED) { 272 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " 273 + "projection callbacks"); 274 } 275 final long token = Binder.clearCallingIdentity(); 276 try { 277 if (mProjectionGrant != null) { 278 mProjectionGrant.stop(); 279 } 280 } finally { 281 Binder.restoreCallingIdentity(token); 282 } 283 284 } 285 286 @Override //Binder call 287 public void addCallback(final IMediaProjectionWatcherCallback callback) { 288 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) 289 != PackageManager.PERMISSION_GRANTED) { 290 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " 291 + "projection callbacks"); 292 } 293 final long token = Binder.clearCallingIdentity(); 294 try { 295 MediaProjectionManagerService.this.addCallback(callback); 296 } finally { 297 Binder.restoreCallingIdentity(token); 298 } 299 } 300 301 @Override 302 public void removeCallback(IMediaProjectionWatcherCallback callback) { 303 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) 304 != PackageManager.PERMISSION_GRANTED) { 305 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove " 306 + "projection callbacks"); 307 } 308 final long token = Binder.clearCallingIdentity(); 309 try { 310 MediaProjectionManagerService.this.removeCallback(callback); 311 } finally { 312 Binder.restoreCallingIdentity(token); 313 } 314 } 315 316 @Override // Binder call 317 public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { 318 if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; 319 final long token = Binder.clearCallingIdentity(); 320 try { 321 MediaProjectionManagerService.this.dump(pw); 322 } finally { 323 Binder.restoreCallingIdentity(token); 324 } 325 } 326 327 328 private boolean checkPermission(String packageName, String permission) { 329 return mContext.getPackageManager().checkPermission(permission, packageName) 330 == PackageManager.PERMISSION_GRANTED; 331 } 332 } 333 334 private final class MediaProjection extends IMediaProjection.Stub { 335 public final int uid; 336 public final String packageName; 337 public final UserHandle userHandle; 338 339 private IMediaProjectionCallback mCallback; 340 private IBinder mToken; 341 private IBinder.DeathRecipient mDeathEater; 342 private int mType; 343 344 public MediaProjection(int type, int uid, String packageName) { 345 mType = type; 346 this.uid = uid; 347 this.packageName = packageName; 348 userHandle = new UserHandle(UserHandle.getUserId(uid)); 349 } 350 351 @Override // Binder call 352 public boolean canProjectVideo() { 353 return mType == MediaProjectionManager.TYPE_MIRRORING || 354 mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE; 355 } 356 357 @Override // Binder call 358 public boolean canProjectSecureVideo() { 359 return false; 360 } 361 362 @Override // Binder call 363 public boolean canProjectAudio() { 364 return mType == MediaProjectionManager.TYPE_MIRRORING || 365 mType == MediaProjectionManager.TYPE_PRESENTATION; 366 } 367 368 @Override // Binder call 369 public int applyVirtualDisplayFlags(int flags) { 370 if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) { 371 flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; 372 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 373 | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; 374 return flags; 375 } else if (mType == MediaProjectionManager.TYPE_MIRRORING) { 376 flags &= ~(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | 377 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR); 378 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | 379 DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; 380 return flags; 381 } else if (mType == MediaProjectionManager.TYPE_PRESENTATION) { 382 flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; 383 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | 384 DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION | 385 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; 386 return flags; 387 } else { 388 throw new RuntimeException("Unknown MediaProjection type"); 389 } 390 } 391 392 @Override // Binder call 393 public void start(final IMediaProjectionCallback callback) { 394 if (callback == null) { 395 throw new IllegalArgumentException("callback must not be null"); 396 } 397 synchronized (mLock) { 398 if (isValidMediaProjection(asBinder())) { 399 throw new IllegalStateException( 400 "Cannot start already started MediaProjection"); 401 } 402 mCallback = callback; 403 registerCallback(mCallback); 404 try { 405 mToken = callback.asBinder(); 406 mDeathEater = new IBinder.DeathRecipient() { 407 @Override 408 public void binderDied() { 409 mCallbackDelegate.remove(callback); 410 stop(); 411 } 412 }; 413 mToken.linkToDeath(mDeathEater, 0); 414 } catch (RemoteException e) { 415 Slog.w(TAG, 416 "MediaProjectionCallbacks must be valid, aborting MediaProjection", e); 417 return; 418 } 419 startProjectionLocked(this); 420 } 421 } 422 423 @Override // Binder call 424 public void stop() { 425 synchronized (mLock) { 426 if (!isValidMediaProjection(asBinder())) { 427 Slog.w(TAG, "Attempted to stop inactive MediaProjection " 428 + "(uid=" + Binder.getCallingUid() + ", " 429 + "pid=" + Binder.getCallingPid() + ")"); 430 return; 431 } 432 stopProjectionLocked(this); 433 mToken.unlinkToDeath(mDeathEater, 0); 434 mToken = null; 435 unregisterCallback(mCallback); 436 mCallback = null; 437 } 438 } 439 440 @Override 441 public void registerCallback(IMediaProjectionCallback callback) { 442 if (callback == null) { 443 throw new IllegalArgumentException("callback must not be null"); 444 } 445 mCallbackDelegate.add(callback); 446 } 447 448 @Override 449 public void unregisterCallback(IMediaProjectionCallback callback) { 450 if (callback == null) { 451 throw new IllegalArgumentException("callback must not be null"); 452 } 453 mCallbackDelegate.remove(callback); 454 } 455 456 public MediaProjectionInfo getProjectionInfo() { 457 return new MediaProjectionInfo(packageName, userHandle); 458 } 459 460 public void dump(PrintWriter pw) { 461 pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType)); 462 } 463 } 464 465 private class MediaRouterCallback extends MediaRouter.SimpleCallback { 466 @Override 467 public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { 468 synchronized (mLock) { 469 if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) { 470 mMediaRouteInfo = info; 471 if (mProjectionGrant != null) { 472 mProjectionGrant.stop(); 473 } 474 } 475 } 476 } 477 478 @Override 479 public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) { 480 if (mMediaRouteInfo == info) { 481 mMediaRouteInfo = null; 482 } 483 } 484 } 485 486 487 private static class CallbackDelegate { 488 private Map<IBinder, IMediaProjectionCallback> mClientCallbacks; 489 private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks; 490 private Handler mHandler; 491 private Object mLock = new Object(); 492 493 public CallbackDelegate() { 494 mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/); 495 mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>(); 496 mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>(); 497 } 498 499 public void add(IMediaProjectionCallback callback) { 500 synchronized (mLock) { 501 mClientCallbacks.put(callback.asBinder(), callback); 502 } 503 } 504 505 public void add(IMediaProjectionWatcherCallback callback) { 506 synchronized (mLock) { 507 mWatcherCallbacks.put(callback.asBinder(), callback); 508 } 509 } 510 511 public void remove(IMediaProjectionCallback callback) { 512 synchronized (mLock) { 513 mClientCallbacks.remove(callback.asBinder()); 514 } 515 } 516 517 public void remove(IMediaProjectionWatcherCallback callback) { 518 synchronized (mLock) { 519 mWatcherCallbacks.remove(callback.asBinder()); 520 } 521 } 522 523 public void dispatchStart(MediaProjection projection) { 524 if (projection == null) { 525 Slog.e(TAG, "Tried to dispatch start notification for a null media projection." 526 + " Ignoring!"); 527 return; 528 } 529 synchronized (mLock) { 530 for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { 531 MediaProjectionInfo info = projection.getProjectionInfo(); 532 mHandler.post(new WatcherStartCallback(info, callback)); 533 } 534 } 535 } 536 537 public void dispatchStop(MediaProjection projection) { 538 if (projection == null) { 539 Slog.e(TAG, "Tried to dispatch stop notification for a null media projection." 540 + " Ignoring!"); 541 return; 542 } 543 synchronized (mLock) { 544 for (IMediaProjectionCallback callback : mClientCallbacks.values()) { 545 mHandler.post(new ClientStopCallback(callback)); 546 } 547 548 for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { 549 MediaProjectionInfo info = projection.getProjectionInfo(); 550 mHandler.post(new WatcherStopCallback(info, callback)); 551 } 552 } 553 } 554 } 555 556 private static final class WatcherStartCallback implements Runnable { 557 private IMediaProjectionWatcherCallback mCallback; 558 private MediaProjectionInfo mInfo; 559 560 public WatcherStartCallback(MediaProjectionInfo info, 561 IMediaProjectionWatcherCallback callback) { 562 mInfo = info; 563 mCallback = callback; 564 } 565 566 @Override 567 public void run() { 568 try { 569 mCallback.onStart(mInfo); 570 } catch (RemoteException e) { 571 Slog.w(TAG, "Failed to notify media projection has stopped", e); 572 } 573 } 574 } 575 576 private static final class WatcherStopCallback implements Runnable { 577 private IMediaProjectionWatcherCallback mCallback; 578 private MediaProjectionInfo mInfo; 579 580 public WatcherStopCallback(MediaProjectionInfo info, 581 IMediaProjectionWatcherCallback callback) { 582 mInfo = info; 583 mCallback = callback; 584 } 585 586 @Override 587 public void run() { 588 try { 589 mCallback.onStop(mInfo); 590 } catch (RemoteException e) { 591 Slog.w(TAG, "Failed to notify media projection has stopped", e); 592 } 593 } 594 } 595 596 private static final class ClientStopCallback implements Runnable { 597 private IMediaProjectionCallback mCallback; 598 599 public ClientStopCallback(IMediaProjectionCallback callback) { 600 mCallback = callback; 601 } 602 603 @Override 604 public void run() { 605 try { 606 mCallback.onStop(); 607 } catch (RemoteException e) { 608 Slog.w(TAG, "Failed to notify media projection has stopped", e); 609 } 610 } 611 } 612 613 614 private static String typeToString(int type) { 615 switch (type) { 616 case MediaProjectionManager.TYPE_SCREEN_CAPTURE: 617 return "TYPE_SCREEN_CAPTURE"; 618 case MediaProjectionManager.TYPE_MIRRORING: 619 return "TYPE_MIRRORING"; 620 case MediaProjectionManager.TYPE_PRESENTATION: 621 return "TYPE_PRESENTATION"; 622 } 623 return Integer.toString(type); 624 } 625 } 626