MediaProjectionManagerService.java revision 4444c5b27874866f18cd8f4abb8914cc17857ea7
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
17package com.android.server.media.projection;
18
19import com.android.server.Watchdog;
20
21import android.Manifest;
22import android.app.AppOpsManager;
23import android.content.Context;
24import android.content.pm.PackageManager;
25import android.hardware.display.DisplayManager;
26import android.media.MediaRouter;
27import android.media.projection.IMediaProjectionManager;
28import android.media.projection.IMediaProjection;
29import android.media.projection.IMediaProjectionCallback;
30import android.media.projection.IMediaProjectionWatcherCallback;
31import android.media.projection.MediaProjectionInfo;
32import android.media.projection.MediaProjectionManager;
33import android.os.Binder;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.IBinder.DeathRecipient;
37import android.os.Looper;
38import android.os.Message;
39import android.os.RemoteException;
40import android.os.UserHandle;
41import android.util.ArrayMap;
42import android.util.Slog;
43
44import com.android.server.SystemService;
45
46import java.io.FileDescriptor;
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.Collection;
50import java.util.List;
51import 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 */
61public 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    }
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                synchronized (mLock) {
131                    removeCallback(callback);
132                }
133            }
134        };
135        synchronized (mLock) {
136            mCallbackDelegate.add(callback);
137            linkDeathRecipientLocked(callback, deathRecipient);
138        }
139    }
140
141    private void removeCallback(IMediaProjectionWatcherCallback callback) {
142        synchronized (mLock) {
143            unlinkDeathRecipientLocked(callback);
144            mCallbackDelegate.remove(callback);
145        }
146    }
147
148    private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback,
149            IBinder.DeathRecipient deathRecipient) {
150        try {
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);
156        }
157    }
158
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);
164        }
165    }
166
167    private void dispatchStart(MediaProjection projection) {
168        mCallbackDelegate.dispatchStart(projection);
169    }
170
171    private void dispatchStop(MediaProjection projection) {
172        mCallbackDelegate.dispatchStop(projection);
173    }
174
175    private boolean isValidMediaProjection(IBinder token) {
176        synchronized (mLock) {
177            if (mProjectionToken != null) {
178                return mProjectionToken.equals(token);
179            }
180            return false;
181        }
182    }
183
184    private MediaProjectionInfo getActiveProjectionInfo() {
185        synchronized (mLock) {
186            if (mProjectionGrant == null) {
187                return null;
188            }
189            return mProjectionGrant.getProjectionInfo();
190        }
191    }
192
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);
199            } else {
200                pw.println("null");
201            }
202        }
203    }
204
205    private final class BinderService extends IMediaProjectionManager.Stub {
206
207        @Override // Binder call
208        public boolean hasProjectionPermission(int uid, String packageName) {
209            long token = Binder.clearCallingIdentity();
210            boolean hasPermission = false;
211            try {
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;
217            } finally {
218                Binder.restoreCallingIdentity(token);
219            }
220            return hasPermission;
221        }
222
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");
230            }
231            if (packageName == null || packageName.isEmpty()) {
232                throw new IllegalArgumentException("package name must not be empty");
233            }
234            long callingToken = Binder.clearCallingIdentity();
235            MediaProjection projection;
236            try {
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);
241                }
242            } finally {
243                Binder.restoreCallingIdentity(callingToken);
244            }
245            return projection;
246        }
247
248        @Override // Binder call
249        public boolean isValidMediaProjection(IMediaProjection projection) {
250            return MediaProjectionManagerService.this.isValidMediaProjection(
251                    projection.asBinder());
252        }
253
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");
260            }
261            final long token = Binder.clearCallingIdentity();
262            try {
263                return MediaProjectionManagerService.this.getActiveProjectionInfo();
264            } finally {
265                Binder.restoreCallingIdentity(token);
266            }
267        }
268
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");
275            }
276            final long token = Binder.clearCallingIdentity();
277            try {
278                if (mProjectionGrant != null) {
279                    mProjectionGrant.stop();
280                }
281            } finally {
282                Binder.restoreCallingIdentity(token);
283            }
284
285        }
286
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");
293            }
294            final long token = Binder.clearCallingIdentity();
295            try {
296                MediaProjectionManagerService.this.addCallback(callback);
297            } finally {
298                Binder.restoreCallingIdentity(token);
299            }
300        }
301
302        @Override
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");
308            }
309            final long token = Binder.clearCallingIdentity();
310            try {
311                MediaProjectionManagerService.this.removeCallback(callback);
312            } finally {
313                Binder.restoreCallingIdentity(token);
314            }
315        }
316
317        @Override // Binder call
318        public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
319            if (mContext == null
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());
324                return;
325            }
326
327            final long token = Binder.clearCallingIdentity();
328            try {
329                dump(pw);
330            } finally {
331                Binder.restoreCallingIdentity(token);
332            }
333        }
334
335
336        private boolean checkPermission(String packageName, String permission) {
337            return mContext.getPackageManager().checkPermission(permission, packageName)
338                    == PackageManager.PERMISSION_GRANTED;
339        }
340    }
341
342    private final class MediaProjection extends IMediaProjection.Stub {
343        public final int uid;
344        public final String packageName;
345        public final UserHandle userHandle;
346
347        private IBinder mToken;
348        private IBinder.DeathRecipient mDeathEater;
349        private int mType;
350
351        public MediaProjection(int type, int uid, String packageName) {
352            mType = type;
353            this.uid = uid;
354            this.packageName = packageName;
355            userHandle = new UserHandle(UserHandle.getUserId(uid));
356        }
357
358        @Override // Binder call
359        public boolean canProjectVideo() {
360            return mType == MediaProjectionManager.TYPE_MIRRORING ||
361                    mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE;
362        }
363
364        @Override // Binder call
365        public boolean canProjectSecureVideo() {
366            return false;
367        }
368
369        @Override // Binder call
370        public boolean canProjectAudio() {
371            return mType == MediaProjectionManager.TYPE_MIRRORING ||
372                    mType == MediaProjectionManager.TYPE_PRESENTATION;
373        }
374
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;
381                return flags;
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;
387                return flags;
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;
393                return flags;
394            } else  {
395                throw new RuntimeException("Unknown MediaProjection type");
396            }
397        }
398
399        @Override // Binder call
400        public void start(final IMediaProjectionCallback callback) {
401            if (callback == null) {
402                throw new IllegalArgumentException("callback must not be null");
403            }
404            synchronized (mLock) {
405                if (isValidMediaProjection(asBinder())) {
406                    throw new IllegalStateException(
407                            "Cannot start already started MediaProjection");
408                }
409                registerCallback(callback);
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                mToken.unlinkToDeath(mDeathEater, 0);
439                stopProjectionLocked(this);
440            }
441        }
442
443        @Override
444        public void registerCallback(IMediaProjectionCallback callback) {
445            if (callback == null) {
446                throw new IllegalArgumentException("callback must not be null");
447            }
448            mCallbackDelegate.add(callback);
449        }
450
451        @Override
452        public void unregisterCallback(IMediaProjectionCallback callback) {
453            if (callback == null) {
454                throw new IllegalArgumentException("callback must not be null");
455            }
456            mCallbackDelegate.remove(callback);
457        }
458
459        public MediaProjectionInfo getProjectionInfo() {
460            return new MediaProjectionInfo(packageName, userHandle);
461        }
462
463        public void dump(PrintWriter pw) {
464            pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
465        }
466    }
467
468    private class MediaRouterCallback extends MediaRouter.SimpleCallback {
469        @Override
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();
476                    }
477                }
478            }
479        }
480
481        @Override
482        public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) {
483            if (mMediaRouteInfo == info) {
484                mMediaRouteInfo = null;
485            }
486        }
487    }
488
489
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();
495
496        public CallbackDelegate() {
497            mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
498            mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>();
499            mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
500        }
501
502        public void add(IMediaProjectionCallback callback) {
503            synchronized (mLock) {
504                mClientCallbacks.put(callback.asBinder(), callback);
505            }
506        }
507
508        public void add(IMediaProjectionWatcherCallback callback) {
509            synchronized (mLock) {
510                mWatcherCallbacks.put(callback.asBinder(), callback);
511            }
512        }
513
514        public void remove(IMediaProjectionCallback callback) {
515            synchronized (mLock) {
516                mClientCallbacks.remove(callback.asBinder());
517            }
518        }
519
520        public void remove(IMediaProjectionWatcherCallback callback) {
521            synchronized (mLock) {
522                mWatcherCallbacks.remove(callback.asBinder());
523            }
524        }
525
526        public void dispatchStart(MediaProjection projection) {
527            if (projection == null) {
528                Slog.e(TAG, "Tried to dispatch start notification for a null media projection."
529                        + " Ignoring!");
530                return;
531            }
532            synchronized (mLock) {
533                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
534                    MediaProjectionInfo info = projection.getProjectionInfo();
535                    mHandler.post(new WatcherStartCallback(info, callback));
536                }
537            }
538        }
539
540        public void dispatchStop(MediaProjection projection) {
541            if (projection == null) {
542                Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
543                        + " Ignoring!");
544                return;
545            }
546            synchronized (mLock) {
547                for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
548                    mHandler.post(new ClientStopCallback(callback));
549                }
550
551                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
552                    MediaProjectionInfo info = projection.getProjectionInfo();
553                    mHandler.post(new WatcherStopCallback(info, callback));
554                }
555            }
556        }
557    }
558
559    private static final class WatcherStartCallback implements Runnable {
560        private IMediaProjectionWatcherCallback mCallback;
561        private MediaProjectionInfo mInfo;
562
563        public WatcherStartCallback(MediaProjectionInfo info,
564                IMediaProjectionWatcherCallback callback) {
565            mInfo = info;
566            mCallback = callback;
567        }
568
569        @Override
570        public void run() {
571            try {
572                mCallback.onStart(mInfo);
573            } catch (RemoteException e) {
574                Slog.w(TAG, "Failed to notify media projection has stopped", e);
575            }
576        }
577    }
578
579    private static final class WatcherStopCallback implements Runnable {
580        private IMediaProjectionWatcherCallback mCallback;
581        private MediaProjectionInfo mInfo;
582
583        public WatcherStopCallback(MediaProjectionInfo info,
584                IMediaProjectionWatcherCallback callback) {
585            mInfo = info;
586            mCallback = callback;
587        }
588
589        @Override
590        public void run() {
591            try {
592                mCallback.onStop(mInfo);
593            } catch (RemoteException e) {
594                Slog.w(TAG, "Failed to notify media projection has stopped", e);
595            }
596        }
597    }
598
599    private static final class ClientStopCallback implements Runnable {
600        private IMediaProjectionCallback mCallback;
601
602        public ClientStopCallback(IMediaProjectionCallback callback) {
603            mCallback = callback;
604        }
605
606        @Override
607        public void run() {
608            try {
609                mCallback.onStop();
610            } catch (RemoteException e) {
611                Slog.w(TAG, "Failed to notify media projection has stopped", e);
612            }
613        }
614    }
615
616
617    private static String typeToString(int type) {
618        switch (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";
625        }
626        return Integer.toString(type);
627    }
628}
629