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