/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.media.projection;
import com.android.server.Watchdog;
import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.display.DisplayManager;
import android.media.MediaRouter;
import android.media.projection.IMediaProjectionManager;
import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Map;
/**
* Manages MediaProjection sessions.
*
* The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections,
* as well as the capabilities they grant. Any service using MediaProjection tokens as permission
* grants must validate the token before use by calling {@link
* IMediaProjectionService#isValidMediaProjection}.
*/
public final class MediaProjectionManagerService extends SystemService
implements Watchdog.Monitor {
private static final String TAG = "MediaProjectionManagerService";
private final Object mLock = new Object(); // Protects the list of media projections
private final Map mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
private final Context mContext;
private final AppOpsManager mAppOps;
private final MediaRouter mMediaRouter;
private final MediaRouterCallback mMediaRouterCallback;
private MediaRouter.RouteInfo mMediaRouteInfo;
private IBinder mProjectionToken;
private MediaProjection mProjectionGrant;
public MediaProjectionManagerService(Context context) {
super(context);
mContext = context;
mDeathEaters = new ArrayMap();
mCallbackDelegate = new CallbackDelegate();
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
mMediaRouterCallback = new MediaRouterCallback();
Watchdog.getInstance().addMonitor(this);
}
@Override
public void onStart() {
publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
false /*allowIsolated*/);
mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
}
@Override
public void onSwitchUser(int userId) {
mMediaRouter.rebindAsUser(userId);
synchronized (mLock) {
if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
}
}
@Override
public void monitor() {
synchronized (mLock) { /* check for deadlock */ }
}
private void startProjectionLocked(final MediaProjection projection) {
if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
if (mMediaRouteInfo != null) {
mMediaRouter.getDefaultRoute().select();
}
mProjectionToken = projection.asBinder();
mProjectionGrant = projection;
dispatchStart(projection);
}
private void stopProjectionLocked(final MediaProjection projection) {
mProjectionToken = null;
mProjectionGrant = null;
dispatchStop(projection);
}
private void addCallback(final IMediaProjectionWatcherCallback callback) {
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
synchronized (mLock) {
removeCallback(callback);
}
}
};
synchronized (mLock) {
mCallbackDelegate.add(callback);
linkDeathRecipientLocked(callback, deathRecipient);
}
}
private void removeCallback(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
unlinkDeathRecipientLocked(callback);
mCallbackDelegate.remove(callback);
}
}
private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback,
IBinder.DeathRecipient deathRecipient) {
try {
final IBinder token = callback.asBinder();
token.linkToDeath(deathRecipient, 0);
mDeathEaters.put(token, deathRecipient);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to link to death for media projection monitoring callback", e);
}
}
private void unlinkDeathRecipientLocked(IMediaProjectionWatcherCallback callback) {
final IBinder token = callback.asBinder();
IBinder.DeathRecipient deathRecipient = mDeathEaters.remove(token);
if (deathRecipient != null) {
token.unlinkToDeath(deathRecipient, 0);
}
}
private void dispatchStart(MediaProjection projection) {
mCallbackDelegate.dispatchStart(projection);
}
private void dispatchStop(MediaProjection projection) {
mCallbackDelegate.dispatchStop(projection);
}
private boolean isValidMediaProjection(IBinder token) {
synchronized (mLock) {
if (mProjectionToken != null) {
return mProjectionToken.equals(token);
}
return false;
}
}
private MediaProjectionInfo getActiveProjectionInfo() {
synchronized (mLock) {
if (mProjectionGrant == null) {
return null;
}
return mProjectionGrant.getProjectionInfo();
}
}
private void dump(final PrintWriter pw) {
pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
synchronized (mLock) {
pw.println("Media Projection: ");
if (mProjectionGrant != null ) {
mProjectionGrant.dump(pw);
} else {
pw.println("null");
}
}
}
private final class BinderService extends IMediaProjectionManager.Stub {
@Override // Binder call
public boolean hasProjectionPermission(int uid, String packageName) {
long token = Binder.clearCallingIdentity();
boolean hasPermission = false;
try {
hasPermission |= checkPermission(packageName,
android.Manifest.permission.CAPTURE_VIDEO_OUTPUT)
|| mAppOps.noteOpNoThrow(
AppOpsManager.OP_PROJECT_MEDIA, uid, packageName)
== AppOpsManager.MODE_ALLOWED;
} finally {
Binder.restoreCallingIdentity(token);
}
return hasPermission;
}
@Override // Binder call
public IMediaProjection createProjection(int uid, String packageName, int type,
boolean isPermanentGrant) {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant "
+ "projection permission");
}
if (packageName == null || packageName.isEmpty()) {
throw new IllegalArgumentException("package name must not be empty");
}
long callingToken = Binder.clearCallingIdentity();
MediaProjection projection;
try {
projection = new MediaProjection(type, uid, packageName);
if (isPermanentGrant) {
mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,
projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);
}
} finally {
Binder.restoreCallingIdentity(callingToken);
}
return projection;
}
@Override // Binder call
public boolean isValidMediaProjection(IMediaProjection projection) {
return MediaProjectionManagerService.this.isValidMediaProjection(
projection.asBinder());
}
@Override // Binder call
public MediaProjectionInfo getActiveProjectionInfo() {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+ "projection callbacks");
}
final long token = Binder.clearCallingIdentity();
try {
return MediaProjectionManagerService.this.getActiveProjectionInfo();
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
public void stopActiveProjection() {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+ "projection callbacks");
}
final long token = Binder.clearCallingIdentity();
try {
if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override //Binder call
public void addCallback(final IMediaProjectionWatcherCallback callback) {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+ "projection callbacks");
}
final long token = Binder.clearCallingIdentity();
try {
MediaProjectionManagerService.this.addCallback(callback);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void removeCallback(IMediaProjectionWatcherCallback callback) {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove "
+ "projection callbacks");
}
final long token = Binder.clearCallingIdentity();
try {
MediaProjectionManagerService.this.removeCallback(callback);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (mContext == null
|| mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump MediaProjectionManager from from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
return;
}
final long token = Binder.clearCallingIdentity();
try {
MediaProjectionManagerService.this.dump(pw);
} finally {
Binder.restoreCallingIdentity(token);
}
}
private boolean checkPermission(String packageName, String permission) {
return mContext.getPackageManager().checkPermission(permission, packageName)
== PackageManager.PERMISSION_GRANTED;
}
}
private final class MediaProjection extends IMediaProjection.Stub {
public final int uid;
public final String packageName;
public final UserHandle userHandle;
private IBinder mToken;
private IBinder.DeathRecipient mDeathEater;
private int mType;
public MediaProjection(int type, int uid, String packageName) {
mType = type;
this.uid = uid;
this.packageName = packageName;
userHandle = new UserHandle(UserHandle.getUserId(uid));
}
@Override // Binder call
public boolean canProjectVideo() {
return mType == MediaProjectionManager.TYPE_MIRRORING ||
mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE;
}
@Override // Binder call
public boolean canProjectSecureVideo() {
return false;
}
@Override // Binder call
public boolean canProjectAudio() {
return mType == MediaProjectionManager.TYPE_MIRRORING ||
mType == MediaProjectionManager.TYPE_PRESENTATION;
}
@Override // Binder call
public int applyVirtualDisplayFlags(int flags) {
if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) {
flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
| DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
return flags;
} else if (mType == MediaProjectionManager.TYPE_MIRRORING) {
flags &= ~(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR);
flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY |
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
return flags;
} else if (mType == MediaProjectionManager.TYPE_PRESENTATION) {
flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC |
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION |
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
return flags;
} else {
throw new RuntimeException("Unknown MediaProjection type");
}
}
@Override // Binder call
public void start(final IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
synchronized (mLock) {
if (isValidMediaProjection(asBinder())) {
throw new IllegalStateException(
"Cannot start already started MediaProjection");
}
registerCallback(callback);
try {
mToken = callback.asBinder();
mDeathEater = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mCallbackDelegate.remove(callback);
stop();
}
};
mToken.linkToDeath(mDeathEater, 0);
} catch (RemoteException e) {
Slog.w(TAG,
"MediaProjectionCallbacks must be valid, aborting MediaProjection", e);
return;
}
startProjectionLocked(this);
}
}
@Override // Binder call
public void stop() {
synchronized (mLock) {
if (!isValidMediaProjection(asBinder())) {
Slog.w(TAG, "Attempted to stop inactive MediaProjection "
+ "(uid=" + Binder.getCallingUid() + ", "
+ "pid=" + Binder.getCallingPid() + ")");
return;
}
mToken.unlinkToDeath(mDeathEater, 0);
stopProjectionLocked(this);
}
}
@Override
public void registerCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
mCallbackDelegate.add(callback);
}
@Override
public void unregisterCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
mCallbackDelegate.remove(callback);
}
public MediaProjectionInfo getProjectionInfo() {
return new MediaProjectionInfo(packageName, userHandle);
}
public void dump(PrintWriter pw) {
pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
}
}
private class MediaRouterCallback extends MediaRouter.SimpleCallback {
@Override
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
synchronized (mLock) {
if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
mMediaRouteInfo = info;
if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
}
}
}
@Override
public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) {
if (mMediaRouteInfo == info) {
mMediaRouteInfo = null;
}
}
}
private static class CallbackDelegate {
private Map mClientCallbacks;
private Map mWatcherCallbacks;
private Handler mHandler;
private Object mLock = new Object();
public CallbackDelegate() {
mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
mClientCallbacks = new ArrayMap();
mWatcherCallbacks = new ArrayMap();
}
public void add(IMediaProjectionCallback callback) {
synchronized (mLock) {
mClientCallbacks.put(callback.asBinder(), callback);
}
}
public void add(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
mWatcherCallbacks.put(callback.asBinder(), callback);
}
}
public void remove(IMediaProjectionCallback callback) {
synchronized (mLock) {
mClientCallbacks.remove(callback.asBinder());
}
}
public void remove(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
mWatcherCallbacks.remove(callback.asBinder());
}
}
public void dispatchStart(MediaProjection projection) {
if (projection == null) {
Slog.e(TAG, "Tried to dispatch start notification for a null media projection."
+ " Ignoring!");
return;
}
synchronized (mLock) {
for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
MediaProjectionInfo info = projection.getProjectionInfo();
mHandler.post(new WatcherStartCallback(info, callback));
}
}
}
public void dispatchStop(MediaProjection projection) {
if (projection == null) {
Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
+ " Ignoring!");
return;
}
synchronized (mLock) {
for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
mHandler.post(new ClientStopCallback(callback));
}
for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
MediaProjectionInfo info = projection.getProjectionInfo();
mHandler.post(new WatcherStopCallback(info, callback));
}
}
}
}
private static final class WatcherStartCallback implements Runnable {
private IMediaProjectionWatcherCallback mCallback;
private MediaProjectionInfo mInfo;
public WatcherStartCallback(MediaProjectionInfo info,
IMediaProjectionWatcherCallback callback) {
mInfo = info;
mCallback = callback;
}
@Override
public void run() {
try {
mCallback.onStart(mInfo);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to notify media projection has stopped", e);
}
}
}
private static final class WatcherStopCallback implements Runnable {
private IMediaProjectionWatcherCallback mCallback;
private MediaProjectionInfo mInfo;
public WatcherStopCallback(MediaProjectionInfo info,
IMediaProjectionWatcherCallback callback) {
mInfo = info;
mCallback = callback;
}
@Override
public void run() {
try {
mCallback.onStop(mInfo);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to notify media projection has stopped", e);
}
}
}
private static final class ClientStopCallback implements Runnable {
private IMediaProjectionCallback mCallback;
public ClientStopCallback(IMediaProjectionCallback callback) {
mCallback = callback;
}
@Override
public void run() {
try {
mCallback.onStop();
} catch (RemoteException e) {
Slog.w(TAG, "Failed to notify media projection has stopped", e);
}
}
}
private static String typeToString(int type) {
switch (type) {
case MediaProjectionManager.TYPE_SCREEN_CAPTURE:
return "TYPE_SCREEN_CAPTURE";
case MediaProjectionManager.TYPE_MIRRORING:
return "TYPE_MIRRORING";
case MediaProjectionManager.TYPE_PRESENTATION:
return "TYPE_PRESENTATION";
}
return Integer.toString(type);
}
}