MediaProjectionManagerService.java revision c39d47a8e7c74bd539104b0efab898ef6fc43ddf
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.MediaProjectionManager;
30import android.os.Binder;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.IBinder.DeathRecipient;
34import android.os.Looper;
35import android.os.Message;
36import android.os.RemoteException;
37import android.util.ArrayMap;
38import android.util.Slog;
39
40import com.android.server.SystemService;
41
42import java.io.FileDescriptor;
43import java.io.PrintWriter;
44import java.util.ArrayList;
45import java.util.Collection;
46import java.util.List;
47import java.util.Map;
48
49/**
50 * Manages MediaProjection sessions.
51 *
52 * The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections,
53 * as well as the capabilities they grant. Any service using MediaProjection tokens as permission
54 * grants <b>must</b> validate the token before use by calling {@link
55 * IMediaProjectionService#isValidMediaProjection}.
56 */
57public final class MediaProjectionManagerService extends SystemService
58        implements Watchdog.Monitor {
59    private static final String TAG = "MediaProjectionManagerService";
60
61    private final Object mLock = new Object(); // Protects the list of media projections
62    private final Map<IBinder, MediaProjection> mProjectionGrants;
63
64    private final Context mContext;
65    private final AppOpsManager mAppOps;
66
67    public MediaProjectionManagerService(Context context) {
68        super(context);
69        mContext = context;
70        mProjectionGrants = new ArrayMap<IBinder, MediaProjection>();
71        mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
72        Watchdog.getInstance().addMonitor(this);
73    }
74
75    @Override
76    public void onStart() {
77        publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
78                false /*allowIsolated*/);
79    }
80
81    @Override
82    public void monitor() {
83        synchronized (mLock) { /* check for deadlock */ }
84    }
85
86    private void dump(final PrintWriter pw) {
87        pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
88        synchronized (mLock) {
89            Collection<MediaProjection> projections = mProjectionGrants.values();
90            pw.println("Media Projections: size=" + projections.size());
91            for (MediaProjection mp : projections) {
92                mp.dump(pw, "  ");
93            }
94        }
95    }
96
97    private final class BinderService extends IMediaProjectionManager.Stub {
98
99        @Override // Binder call
100        public boolean hasProjectionPermission(int uid, String packageName) {
101            long token = Binder.clearCallingIdentity();
102            boolean hasPermission = false;
103            try {
104                hasPermission |= checkPermission(packageName,
105                        android.Manifest.permission.CAPTURE_VIDEO_OUTPUT)
106                        || mAppOps.checkOpNoThrow(
107                                AppOpsManager.OP_PROJECT_MEDIA, uid, packageName)
108                        == AppOpsManager.MODE_ALLOWED;
109            } finally {
110                Binder.restoreCallingIdentity(token);
111            }
112            return hasPermission;
113        }
114
115        @Override // Binder call
116        public IMediaProjection createProjection(int uid, String packageName, int type,
117                boolean isPermanentGrant) {
118            if (mContext.checkCallingPermission(Manifest.permission.CREATE_MEDIA_PROJECTION)
119                        != PackageManager.PERMISSION_GRANTED) {
120                throw new SecurityException("Requires CREATE_MEDIA_PROJECTION in order to grant "
121                        + "projection permission");
122            }
123            long callingToken = Binder.clearCallingIdentity();
124            MediaProjection projection;
125            try {
126                projection = new MediaProjection(type, uid, packageName);
127                if (isPermanentGrant) {
128                    mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,
129                            projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);
130                }
131            } finally {
132                Binder.restoreCallingIdentity(callingToken);
133            }
134            return projection;
135        }
136
137        @Override // Binder call
138        public boolean isValidMediaProjection(IMediaProjection projection) {
139            return mProjectionGrants.containsKey(projection.asBinder());
140        }
141
142        @Override // Binder call
143        public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
144            if (mContext == null
145                    || mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
146                    != PackageManager.PERMISSION_GRANTED) {
147                pw.println("Permission Denial: can't dump MediaProjectionManager from from pid="
148                        + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
149                return;
150            }
151
152            final long token = Binder.clearCallingIdentity();
153            try {
154                dump(pw);
155            } finally {
156                Binder.restoreCallingIdentity(token);
157            }
158        }
159
160        private boolean checkPermission(String packageName, String permission) {
161            return mContext.getPackageManager().checkPermission(permission, packageName)
162                    == PackageManager.PERMISSION_GRANTED;
163        }
164    }
165
166    private final class MediaProjection extends IMediaProjection.Stub implements DeathRecipient {
167        public int uid;
168        public String packageName;
169
170        private IBinder mToken;
171        private int mType;
172        private CallbackDelegate mCallbackDelegate;
173
174        public MediaProjection(int type, int uid, String packageName) {
175            mType = type;
176            this.uid = uid;
177            this.packageName = packageName;
178            mCallbackDelegate = new CallbackDelegate();
179        }
180
181        @Override // Binder call
182        public boolean canProjectVideo() {
183            return mType == MediaProjectionManager.TYPE_MIRRORING ||
184                    mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE;
185        }
186
187        @Override // Binder call
188        public boolean canProjectSecureVideo() {
189            return false;
190        }
191
192        @Override // Binder call
193        public boolean canProjectAudio() {
194            return mType == MediaProjectionManager.TYPE_MIRRORING ||
195                    mType == MediaProjectionManager.TYPE_PRESENTATION;
196        }
197
198        @Override // Binder call
199        public int getVirtualDisplayFlags() {
200            switch (mType) {
201                case MediaProjectionManager.TYPE_SCREEN_CAPTURE:
202                    return DisplayManager.VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE;
203                case MediaProjectionManager.TYPE_MIRRORING:
204                    return DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
205                            DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
206                case MediaProjectionManager.TYPE_PRESENTATION:
207                    return DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION |
208                            DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
209            }
210            throw new RuntimeException("Unknown MediaProjection type");
211        }
212
213        @Override // Binder call
214        public void start(IMediaProjectionCallback callback) {
215            if (callback == null) {
216                throw new IllegalArgumentException("callback must not be null");
217            }
218            synchronized (mLock) {
219                if (mProjectionGrants.containsKey(asBinder())) {
220                    throw new IllegalStateException(
221                            "Cannot start already started MediaProjection");
222                }
223                addCallback(callback);
224                try {
225                    mToken = callback.asBinder();
226                    mToken.linkToDeath(this, 0);
227                } catch (RemoteException e) {
228                    Slog.w(TAG,
229                            "MediaProjectionCallbacks must be valid, aborting MediaProjection", e);
230                    return;
231                }
232                mProjectionGrants.put(asBinder(), this);
233            }
234        }
235
236        @Override // Binder call
237        public void stop() {
238            synchronized (mLock) {
239                if (!mProjectionGrants.containsKey(asBinder())) {
240                    Slog.w(TAG, "Attempted to stop inactive MediaProjection "
241                            + "(uid=" + Binder.getCallingUid() + ", "
242                            + "pid=" + Binder.getCallingPid() + ")");
243                    return;
244                }
245                mToken.unlinkToDeath(this, 0);
246                mCallbackDelegate.dispatchStop();
247                mProjectionGrants.remove(asBinder());
248            }
249        }
250
251        @Override
252        public void binderDied() {
253            stop();
254        }
255
256        @Override
257        public void addCallback(IMediaProjectionCallback callback) {
258            if (callback == null) {
259                throw new IllegalArgumentException("callback must not be null");
260            }
261            mCallbackDelegate.add(callback);
262        }
263
264        @Override
265        public void removeCallback(IMediaProjectionCallback callback) {
266            if (callback == null) {
267                throw new IllegalArgumentException("callback must not be null");
268            }
269            mCallbackDelegate.remove(callback);
270        }
271
272        public void dump(PrintWriter pw, String prefix) {
273            pw.println(prefix + "(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
274        }
275    }
276
277    private static class CallbackDelegate {
278        private static final int MSG_ON_STOP = 0;
279        private List<IMediaProjectionCallback> mCallbacks;
280        private Handler mHandler;
281        private Object mLock = new Object();
282
283        public CallbackDelegate() {
284            mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
285            mCallbacks = new ArrayList<IMediaProjectionCallback>();
286        }
287
288        public void add(IMediaProjectionCallback callback) {
289            synchronized (mLock) {
290                mCallbacks.add(callback);
291            }
292        }
293
294        public void remove(IMediaProjectionCallback callback) {
295            synchronized (mLock) {
296                mCallbacks.remove(callback);
297            }
298        }
299
300        public void dispatchStop() {
301            synchronized (mLock) {
302                for (final IMediaProjectionCallback callback : mCallbacks) {
303                    mHandler.post(new Runnable() {
304                        @Override
305                        public void run() {
306                            try {
307                                callback.onStop();
308                            } catch (RemoteException e) {
309                                Slog.w(TAG, "Failed to notify media projection has stopped", e);
310                            }
311                        }
312                    });
313                }
314            }
315        }
316    }
317
318    private static String typeToString(int type) {
319        switch (type) {
320            case MediaProjectionManager.TYPE_SCREEN_CAPTURE:
321                return "TYPE_SCREEN_CAPTURE";
322            case MediaProjectionManager.TYPE_MIRRORING:
323                return "TYPE_MIRRORING";
324            case MediaProjectionManager.TYPE_PRESENTATION:
325                return "TYPE_PRESENTATION";
326        }
327        return Integer.toString(type);
328    }
329}
330