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