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