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