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