Home | History | Annotate | Download | only in projection
      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