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