MediaProjectionManagerService.java revision 89c2cb6f31cddb926dea3498f8adf01a8cc1426a
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    }
96
97    @Override
98    public void onSwitchUser(int userId) {
99        mMediaRouter.rebindAsUser(userId);
100    }
101
102    @Override
103    public void monitor() {
104        synchronized (mLock) { /* check for deadlock */ }
105    }
106
107    private void startProjectionLocked(final MediaProjection projection) {
108        if (mProjectionGrant != null) {
109            mProjectionGrant.stop();
110        }
111        if (mMediaRouteInfo != null) {
112            mMediaRouter.getDefaultRoute().select();
113        }
114        mProjectionToken = projection.asBinder();
115        mProjectionGrant = projection;
116        dispatchStart(projection);
117    }
118
119    private void stopProjectionLocked(final MediaProjection projection) {
120        mProjectionToken = null;
121        mProjectionGrant = null;
122        dispatchStop(projection);
123    }
124
125    private void addCallback(final IMediaProjectionWatcherCallback callback) {
126        IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
127            @Override
128            public void binderDied() {
129                synchronized (mLock) {
130                    removeCallback(callback);
131                }
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 (mContext == null
319                    || mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
320                    != PackageManager.PERMISSION_GRANTED) {
321                pw.println("Permission Denial: can't dump MediaProjectionManager from from pid="
322                        + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
323                return;
324            }
325
326            final long token = Binder.clearCallingIdentity();
327            try {
328                dump(pw);
329            } finally {
330                Binder.restoreCallingIdentity(token);
331            }
332        }
333
334
335        private boolean checkPermission(String packageName, String permission) {
336            return mContext.getPackageManager().checkPermission(permission, packageName)
337                    == PackageManager.PERMISSION_GRANTED;
338        }
339    }
340
341    private final class MediaProjection extends IMediaProjection.Stub {
342        public final int uid;
343        public final String packageName;
344        public final UserHandle userHandle;
345
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                registerCallback(callback);
409                try {
410                    mToken = callback.asBinder();
411                    mDeathEater = new IBinder.DeathRecipient() {
412                        @Override
413                        public void binderDied() {
414                            mCallbackDelegate.remove(callback);
415                            stop();
416                        }
417                    };
418                    mToken.linkToDeath(mDeathEater, 0);
419                } catch (RemoteException e) {
420                    Slog.w(TAG,
421                            "MediaProjectionCallbacks must be valid, aborting MediaProjection", e);
422                    return;
423                }
424                startProjectionLocked(this);
425            }
426        }
427
428        @Override // Binder call
429        public void stop() {
430            synchronized (mLock) {
431                if (!isValidMediaProjection(asBinder())) {
432                    Slog.w(TAG, "Attempted to stop inactive MediaProjection "
433                            + "(uid=" + Binder.getCallingUid() + ", "
434                            + "pid=" + Binder.getCallingPid() + ")");
435                    return;
436                }
437                mToken.unlinkToDeath(mDeathEater, 0);
438                stopProjectionLocked(this);
439            }
440        }
441
442        @Override
443        public void registerCallback(IMediaProjectionCallback callback) {
444            if (callback == null) {
445                throw new IllegalArgumentException("callback must not be null");
446            }
447            mCallbackDelegate.add(callback);
448        }
449
450        @Override
451        public void unregisterCallback(IMediaProjectionCallback callback) {
452            if (callback == null) {
453                throw new IllegalArgumentException("callback must not be null");
454            }
455            mCallbackDelegate.remove(callback);
456        }
457
458        public MediaProjectionInfo getProjectionInfo() {
459            return new MediaProjectionInfo(packageName, userHandle);
460        }
461
462        public void dump(PrintWriter pw) {
463            pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
464        }
465    }
466
467    private class MediaRouterCallback extends MediaRouter.SimpleCallback {
468        @Override
469        public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
470            synchronized (mLock) {
471                if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
472                    mMediaRouteInfo = info;
473                    if (mProjectionGrant != null) {
474                        mProjectionGrant.stop();
475                    }
476                }
477            }
478        }
479
480        @Override
481        public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) {
482            if (mMediaRouteInfo == info) {
483                mMediaRouteInfo = null;
484            }
485        }
486    }
487
488
489    private static class CallbackDelegate {
490        private Map<IBinder, IMediaProjectionCallback> mClientCallbacks;
491        private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks;
492        private Handler mHandler;
493        private Object mLock = new Object();
494
495        public CallbackDelegate() {
496            mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
497            mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>();
498            mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
499        }
500
501        public void add(IMediaProjectionCallback callback) {
502            synchronized (mLock) {
503                mClientCallbacks.put(callback.asBinder(), callback);
504            }
505        }
506
507        public void add(IMediaProjectionWatcherCallback callback) {
508            synchronized (mLock) {
509                mWatcherCallbacks.put(callback.asBinder(), callback);
510            }
511        }
512
513        public void remove(IMediaProjectionCallback callback) {
514            synchronized (mLock) {
515                mClientCallbacks.remove(callback.asBinder());
516            }
517        }
518
519        public void remove(IMediaProjectionWatcherCallback callback) {
520            synchronized (mLock) {
521                mWatcherCallbacks.remove(callback.asBinder());
522            }
523        }
524
525        public void dispatchStart(MediaProjection projection) {
526            if (projection == null) {
527                Slog.e(TAG, "Tried to dispatch start notification for a null media projection."
528                        + " Ignoring!");
529                return;
530            }
531            synchronized (mLock) {
532                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
533                    MediaProjectionInfo info = projection.getProjectionInfo();
534                    mHandler.post(new WatcherStartCallback(info, callback));
535                }
536            }
537        }
538
539        public void dispatchStop(MediaProjection projection) {
540            if (projection == null) {
541                Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
542                        + " Ignoring!");
543                return;
544            }
545            synchronized (mLock) {
546                for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
547                    mHandler.post(new ClientStopCallback(callback));
548                }
549
550                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
551                    MediaProjectionInfo info = projection.getProjectionInfo();
552                    mHandler.post(new WatcherStopCallback(info, callback));
553                }
554            }
555        }
556    }
557
558    private static final class WatcherStartCallback implements Runnable {
559        private IMediaProjectionWatcherCallback mCallback;
560        private MediaProjectionInfo mInfo;
561
562        public WatcherStartCallback(MediaProjectionInfo info,
563                IMediaProjectionWatcherCallback callback) {
564            mInfo = info;
565            mCallback = callback;
566        }
567
568        @Override
569        public void run() {
570            try {
571                mCallback.onStart(mInfo);
572            } catch (RemoteException e) {
573                Slog.w(TAG, "Failed to notify media projection has stopped", e);
574            }
575        }
576    }
577
578    private static final class WatcherStopCallback implements Runnable {
579        private IMediaProjectionWatcherCallback mCallback;
580        private MediaProjectionInfo mInfo;
581
582        public WatcherStopCallback(MediaProjectionInfo info,
583                IMediaProjectionWatcherCallback callback) {
584            mInfo = info;
585            mCallback = callback;
586        }
587
588        @Override
589        public void run() {
590            try {
591                mCallback.onStop(mInfo);
592            } catch (RemoteException e) {
593                Slog.w(TAG, "Failed to notify media projection has stopped", e);
594            }
595        }
596    }
597
598    private static final class ClientStopCallback implements Runnable {
599        private IMediaProjectionCallback mCallback;
600
601        public ClientStopCallback(IMediaProjectionCallback callback) {
602            mCallback = callback;
603        }
604
605        @Override
606        public void run() {
607            try {
608                mCallback.onStop();
609            } catch (RemoteException e) {
610                Slog.w(TAG, "Failed to notify media projection has stopped", e);
611            }
612        }
613    }
614
615
616    private static String typeToString(int type) {
617        switch (type) {
618            case MediaProjectionManager.TYPE_SCREEN_CAPTURE:
619                return "TYPE_SCREEN_CAPTURE";
620            case MediaProjectionManager.TYPE_MIRRORING:
621                return "TYPE_MIRRORING";
622            case MediaProjectionManager.TYPE_PRESENTATION:
623                return "TYPE_PRESENTATION";
624        }
625        return Integer.toString(type);
626    }
627}
628