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.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