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