2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.server.media.projection;
19 import com.android.server.Watchdog;
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;
44 import com.android.server.SystemService;
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;
54 * Manages MediaProjection sessions.
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}.
61 public final class MediaProjectionManagerService extends SystemService
62 implements Watchdog.Monitor {
63 private static final String TAG = "MediaProjectionManagerService";
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;
69 private final Context mContext;
70 private final AppOpsManager mAppOps;
72 private final MediaRouter mMediaRouter;
73 private final MediaRouterCallback mMediaRouterCallback;
74 private MediaRouter.RouteInfo mMediaRouteInfo;
76 private IBinder mProjectionToken;
77 private MediaProjection mProjectionGrant;
79 public MediaProjectionManagerService(Context 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);
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);
99 public void onSwitchUser(int userId) {
100 mMediaRouter.rebindAsUser(userId);
104 public void monitor() {
105 synchronized (mLock) { /* check for deadlock */ }
108 private void startProjectionLocked(final MediaProjection projection) {
109 if (mProjectionGrant != null) {
110 mProjectionGrant.stop();
112 if (mMediaRouteInfo != null) {
113 mMediaRouter.getDefaultRoute().select();
115 mProjectionToken = projection.asBinder();
116 mProjectionGrant = projection;
117 dispatchStart(projection);
120 private void stopProjectionLocked(final MediaProjection projection) {
121 mProjectionToken = null;
122 mProjectionGrant = null;
123 dispatchStop(projection);
126 private void addCallback(final IMediaProjectionWatcherCallback callback) {
127 IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
129 public void binderDied() {
130 synchronized (mLock) {
131 removeCallback(callback);
135 synchronized (mLock) {
136 mCallbackDelegate.add(callback);
137 linkDeathRecipientLocked(callback, deathRecipient);
141 private void removeCallback(IMediaProjectionWatcherCallback callback) {
142 synchronized (mLock) {
143 unlinkDeathRecipientLocked(callback);
144 mCallbackDelegate.remove(callback);
148 private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback,
149 IBinder.DeathRecipient deathRecipient) {
151 final IBinder token = callback.asBinder();
152 token.linkToDeath(deathRecipient, 0);
153 mDeathEaters.put(token, deathRecipient);
154 } catch (RemoteException e) {
155 Slog.e(TAG, "Unable to link to death for media projection monitoring callback", e);
159 private void unlinkDeathRecipientLocked(IMediaProjectionWatcherCallback callback) {
160 final IBinder token = callback.asBinder();
161 IBinder.DeathRecipient deathRecipient = mDeathEaters.remove(token);
162 if (deathRecipient != null) {
163 token.unlinkToDeath(deathRecipient, 0);
167 private void dispatchStart(MediaProjection projection) {
168 mCallbackDelegate.dispatchStart(projection);
171 private void dispatchStop(MediaProjection projection) {
172 mCallbackDelegate.dispatchStop(projection);
175 private boolean isValidMediaProjection(IBinder token) {
176 synchronized (mLock) {
177 if (mProjectionToken != null) {
178 return mProjectionToken.equals(token);
184 private MediaProjectionInfo getActiveProjectionInfo() {
185 synchronized (mLock) {
186 if (mProjectionGrant == null) {
189 return mProjectionGrant.getProjectionInfo();
193 private void dump(final PrintWriter pw) {
194 pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
195 synchronized (mLock) {
196 pw.println("Media Projection: ");
197 if (mProjectionGrant != null ) {
198 mProjectionGrant.dump(pw);
205 private final class BinderService extends IMediaProjectionManager.Stub {
207 @Override // Binder call
208 public boolean hasProjectionPermission(int uid, String packageName) {
209 long token = Binder.clearCallingIdentity();
210 boolean hasPermission = false;
212 hasPermission |= checkPermission(packageName,
213 android.Manifest.permission.CAPTURE_VIDEO_OUTPUT)
214 || mAppOps.noteOpNoThrow(
215 AppOpsManager.OP_PROJECT_MEDIA, uid, packageName)
216 == AppOpsManager.MODE_ALLOWED;
218 Binder.restoreCallingIdentity(token);
220 return hasPermission;
223 @Override // Binder call
224 public IMediaProjection createProjection(int uid, String packageName, int type,
225 boolean isPermanentGrant) {
226 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
227 != PackageManager.PERMISSION_GRANTED) {
228 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant "
229 + "projection permission");
231 if (packageName == null || packageName.isEmpty()) {
232 throw new IllegalArgumentException("package name must not be empty");
234 long callingToken = Binder.clearCallingIdentity();
235 MediaProjection projection;
237 projection = new MediaProjection(type, uid, packageName);
238 if (isPermanentGrant) {
239 mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,
240 projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);
243 Binder.restoreCallingIdentity(callingToken);
248 @Override // Binder call
249 public boolean isValidMediaProjection(IMediaProjection projection) {
250 return MediaProjectionManagerService.this.isValidMediaProjection(
251 projection.asBinder());
254 @Override // Binder call
255 public MediaProjectionInfo getActiveProjectionInfo() {
256 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
257 != PackageManager.PERMISSION_GRANTED) {
258 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
259 + "projection callbacks");
261 final long token = Binder.clearCallingIdentity();
263 return MediaProjectionManagerService.this.getActiveProjectionInfo();
265 Binder.restoreCallingIdentity(token);
269 @Override // Binder call
270 public void stopActiveProjection() {
271 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
272 != PackageManager.PERMISSION_GRANTED) {
273 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
274 + "projection callbacks");
276 final long token = Binder.clearCallingIdentity();
278 if (mProjectionGrant != null) {
279 mProjectionGrant.stop();
282 Binder.restoreCallingIdentity(token);
287 @Override //Binder call
288 public void addCallback(final IMediaProjectionWatcherCallback callback) {
289 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
290 != PackageManager.PERMISSION_GRANTED) {
291 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
292 + "projection callbacks");
294 final long token = Binder.clearCallingIdentity();
296 MediaProjectionManagerService.this.addCallback(callback);
298 Binder.restoreCallingIdentity(token);
303 public void removeCallback(IMediaProjectionWatcherCallback callback) {
304 if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
305 != PackageManager.PERMISSION_GRANTED) {
306 throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove "
307 + "projection callbacks");
309 final long token = Binder.clearCallingIdentity();
311 MediaProjectionManagerService.this.removeCallback(callback);
313 Binder.restoreCallingIdentity(token);
317 @Override // Binder call
318 public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
320 || mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
321 != PackageManager.PERMISSION_GRANTED) {
322 pw.println("Permission Denial: can't dump MediaProjectionManager from from pid="
323 + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
327 final long token = Binder.clearCallingIdentity();
329 MediaProjectionManagerService.this.dump(pw);
331 Binder.restoreCallingIdentity(token);
336 private boolean checkPermission(String packageName, String permission) {
337 return mContext.getPackageManager().checkPermission(permission, packageName)
338 == PackageManager.PERMISSION_GRANTED;
342 private final class MediaProjection extends IMediaProjection.Stub {
343 public final int uid;
344 public final String packageName;
345 public final UserHandle userHandle;
347 private IBinder mToken;
348 private IBinder.DeathRecipient mDeathEater;
351 public MediaProjection(int type, int uid, String packageName) {
354 this.packageName = packageName;
355 userHandle = new UserHandle(UserHandle.getUserId(uid));
358 @Override // Binder call
359 public boolean canProjectVideo() {
360 return mType == MediaProjectionManager.TYPE_MIRRORING ||
361 mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE;
364 @Override // Binder call
365 public boolean canProjectSecureVideo() {
369 @Override // Binder call
370 public boolean canProjectAudio() {
371 return mType == MediaProjectionManager.TYPE_MIRRORING ||
372 mType == MediaProjectionManager.TYPE_PRESENTATION;
375 @Override // Binder call
376 public int applyVirtualDisplayFlags(int flags) {
377 if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) {
378 flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
379 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
380 | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
382 } else if (mType == MediaProjectionManager.TYPE_MIRRORING) {
383 flags &= ~(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
384 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR);
385 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY |
386 DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
388 } else if (mType == MediaProjectionManager.TYPE_PRESENTATION) {
389 flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
390 flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
391 DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION |
392 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
395 throw new RuntimeException("Unknown MediaProjection type");
399 @Override // Binder call
400 public void start(final IMediaProjectionCallback callback) {
401 if (callback == null) {
402 throw new IllegalArgumentException("callback must not be null");
404 synchronized (mLock) {
405 if (isValidMediaProjection(asBinder())) {
406 throw new IllegalStateException(
407 "Cannot start already started MediaProjection");
409 registerCallback(callback);
411 mToken = callback.asBinder();
412 mDeathEater = new IBinder.DeathRecipient() {
414 public void binderDied() {
415 mCallbackDelegate.remove(callback);
419 mToken.linkToDeath(mDeathEater, 0);
420 } catch (RemoteException e) {
422 "MediaProjectionCallbacks must be valid, aborting MediaProjection", e);
425 startProjectionLocked(this);
429 @Override // Binder call
431 synchronized (mLock) {
432 if (!isValidMediaProjection(asBinder())) {
433 Slog.w(TAG, "Attempted to stop inactive MediaProjection "
434 + "(uid=" + Binder.getCallingUid() + ", "
435 + "pid=" + Binder.getCallingPid() + ")");
438 mToken.unlinkToDeath(mDeathEater, 0);
439 stopProjectionLocked(this);
444 public void registerCallback(IMediaProjectionCallback callback) {
445 if (callback == null) {
446 throw new IllegalArgumentException("callback must not be null");
448 mCallbackDelegate.add(callback);
452 public void unregisterCallback(IMediaProjectionCallback callback) {
453 if (callback == null) {
454 throw new IllegalArgumentException("callback must not be null");
456 mCallbackDelegate.remove(callback);
459 public MediaProjectionInfo getProjectionInfo() {
460 return new MediaProjectionInfo(packageName, userHandle);
463 public void dump(PrintWriter pw) {
464 pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
468 private class MediaRouterCallback extends MediaRouter.SimpleCallback {
470 public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
471 synchronized (mLock) {
472 if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
473 mMediaRouteInfo = info;
474 if (mProjectionGrant != null) {
475 mProjectionGrant.stop();
482 public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) {
483 if (mMediaRouteInfo == info) {
484 mMediaRouteInfo = null;
490 private static class CallbackDelegate {
491 private Map<IBinder, IMediaProjectionCallback> mClientCallbacks;
492 private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks;
493 private Handler mHandler;
494 private Object mLock = new Object();
496 public CallbackDelegate() {
497 mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
498 mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>();
499 mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
502 public void add(IMediaProjectionCallback callback) {
503 synchronized (mLock) {
504 mClientCallbacks.put(callback.asBinder(), callback);
508 public void add(IMediaProjectionWatcherCallback callback) {
509 synchronized (mLock) {
510 mWatcherCallbacks.put(callback.asBinder(), callback);
514 public void remove(IMediaProjectionCallback callback) {
515 synchronized (mLock) {
516 mClientCallbacks.remove(callback.asBinder());
520 public void remove(IMediaProjectionWatcherCallback callback) {
521 synchronized (mLock) {
522 mWatcherCallbacks.remove(callback.asBinder());
526 public void dispatchStart(MediaProjection projection) {
527 if (projection == null) {
528 Slog.e(TAG, "Tried to dispatch start notification for a null media projection."
532 synchronized (mLock) {
533 for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
534 MediaProjectionInfo info = projection.getProjectionInfo();
535 mHandler.post(new WatcherStartCallback(info, callback));
540 public void dispatchStop(MediaProjection projection) {
541 if (projection == null) {
542 Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
546 synchronized (mLock) {
547 for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
548 mHandler.post(new ClientStopCallback(callback));
551 for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
552 MediaProjectionInfo info = projection.getProjectionInfo();
553 mHandler.post(new WatcherStopCallback(info, callback));
559 private static final class WatcherStartCallback implements Runnable {
560 private IMediaProjectionWatcherCallback mCallback;
561 private MediaProjectionInfo mInfo;
563 public WatcherStartCallback(MediaProjectionInfo info,
564 IMediaProjectionWatcherCallback callback) {
566 mCallback = callback;
572 mCallback.onStart(mInfo);
573 } catch (RemoteException e) {
574 Slog.w(TAG, "Failed to notify media projection has stopped", e);
579 private static final class WatcherStopCallback implements Runnable {
580 private IMediaProjectionWatcherCallback mCallback;
581 private MediaProjectionInfo mInfo;
583 public WatcherStopCallback(MediaProjectionInfo info,
584 IMediaProjectionWatcherCallback callback) {
586 mCallback = callback;
592 mCallback.onStop(mInfo);
593 } catch (RemoteException e) {
594 Slog.w(TAG, "Failed to notify media projection has stopped", e);
599 private static final class ClientStopCallback implements Runnable {
600 private IMediaProjectionCallback mCallback;
602 public ClientStopCallback(IMediaProjectionCallback callback) {
603 mCallback = callback;
610 } catch (RemoteException e) {
611 Slog.w(TAG, "Failed to notify media projection has stopped", e);
617 private static String typeToString(int type) {
619 case MediaProjectionManager.TYPE_SCREEN_CAPTURE:
620 return "TYPE_SCREEN_CAPTURE";
621 case MediaProjectionManager.TYPE_MIRRORING:
622 return "TYPE_MIRRORING";
623 case MediaProjectionManager.TYPE_PRESENTATION:
624 return "TYPE_PRESENTATION";
626 return Integer.toString(type);