1.概述
Android原生对MediaProjection的管理逻辑,是如果服务端已经保存有MediaProjection的实例,那么再次创建的时候,之前的MediaProjection实例就会被暂停,并且引用指向新的实例,也就导致了当开启后一个录屏应用时,前一个录屏应用会被暂停。为了能实现多个录屏应用同时录屏(或者共享屏幕或投屏的时候录屏),我们需要对framework进行修改,只需对一个文件进行修改:frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
2.实现能同时开启多个MediaProjection
大致的思路就是修改MediaProjectionManagerService.java,使得MediaProjectionManagerService服务端能够保存多个MediaProjeciton实例。
/*
* 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 android.Manifest;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IProcessObserver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.hardware.display.DisplayManager;
import android.media.MediaRouter;
import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionManager;
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.Watchdog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
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 <b>must</b> validate the token before use by calling {@link
* IMediaProjectionService#isValidMediaProjection}.
*/
public final class MediaProjectionManagerService extends SystemService
implements Watchdog.Monitor {
private static final boolean REQUIRE_FG_SERVICE_FOR_PROJECTION = true;
private static final String TAG = "MediaProjectionManagerService";
private static boolean DEBUG = false;
private final Object mLock = new Object(); // Protects the list of media projections
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
private final Context mContext;
private final AppOpsManager mAppOps;
private final ActivityManagerInternal mActivityManagerInternal;
private final PackageManager mPackageManager;
private final MediaRouter mMediaRouter;
private final MediaRouterCallback mMediaRouterCallback;
private MediaRouter.RouteInfo mMediaRouteInfo;
private Map<IBinder, MediaProjectionEntry> mEntryMap;
public MediaProjectionManagerService(Context context) {
super(context);
mContext = context;
mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
mCallbackDelegate = new CallbackDelegate();
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mPackageManager = mContext.getPackageManager();
mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
mMediaRouterCallback = new MediaRouterCallback();
mEntryMap = new ArrayMap<IBinder, MediaProjectionEntry>();
if ("userdebug".equals(SystemProperties.get("ro.build.type", "user"))) {
DEBUG = true;
}
Watchdog.getInstance().addMonitor(this);
}
@Override
public void onStart() {
logd("onStart");
publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
false /*allowIsolated*/);
mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
if (REQUIRE_FG_SERVICE_FOR_PROJECTION) {
mActivityManagerInternal.registerProcessObserver(new IProcessObserver.Stub() {
@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) {
}
@Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
MediaProjectionManagerService.this.handleForegroundServicesChanged(pid, uid,
serviceTypes);
}
@Override
public void onProcessDied(int pid, int uid) {
}
});
}
}
@Override
public void onSwitchUser(int userId) {
logd("onSwitchUser: userId=" + userId);
mMediaRouter.rebindAsUser(userId);
synchronized (mLock) {
if (mEntryMap != null && !mEntryMap.isEmpty()) {
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null) {
entry.getProjectionGrant().stop();
}
}
}
}
}
@Override
public void monitor() {
synchronized (mLock) { /* check for deadlock */ }
}
/**
* Called when the set of active foreground service types for a given {@code uid / pid} changes.
* We will stop the active projection grant if its owner targets {@code Q} or higher and has no
* started foreground services of type {@code FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
*/
private void handleForegroundServicesChanged(int pid, int uid, int serviceTypes) {
synchronized (mLock) {
logd("handleForegroundServicesChanged: pid=" + pid + ", uid=" + uid
+ ", serviceTypes=" + serviceTypes);
if (mEntryMap == null || mEntryMap.isEmpty()) {
return;
}
String uidpid = String.format("%s_%s", uid, pid);
MediaProjectionEntry entry = getMediaProjectionEntryByUidPid(uidpid);
if (entry == null || entry.getProjectionGrant() == null) {
return;
}
MediaProjection mediaProjection = entry.getProjectionGrant();
if (!mediaProjection.requiresForegroundService()) {
return;
}
if ((serviceTypes & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) != 0) {
return;
}
mediaProjection.stop();
logd("handleForegroundServicesChanged: MediaProjection="
+ mediaProjection.getProjectionInfo() + " hashCode "
+ mediaProjection.asBinder().hashCode() + " stoped");
}
}
private void startProjectionLocked(final String uidpid, final MediaProjection projection) {
logd("startProjectionLocked: calling uidpid " + uidpid
+ ", MediaProjection=" + projection.getProjectionInfo()
+ " hashCode=" + projection.asBinder().hashCode()
+ ", current MediaProjection count " + mEntryMap.size());
if (mMediaRouteInfo != null) {
mMediaRouter.getFallbackRoute().select();
}
List<IBinder> removedMediaProjections = new ArrayList<>();
logd(">>> MediaProjections:");
if (mEntryMap.isEmpty()) {
logd(">>> null");
} else {
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null) {
logd(">>> " + entry.getProjectionGrant().getProjectionInfo()
+ " hashCode " + entry.getProjectionToken().hashCode());
MediaProjection mp = entry.getProjectionGrant();
if (mp.packageName.equals(projection.packageName)) {
removedMediaProjections.add(entry.getProjectionToken());
}
} else {
logd(">>> null");
}
}
}
for (IBinder token : removedMediaProjections) {
MediaProjectionEntry entry = mEntryMap.get(token);
if (entry != null) {
MediaProjection mp = entry.getProjectionGrant();
logd(mp.packageName + " already existing MediaProjection="
+ mp.getProjectionInfo() + " hashCode " + mp.asBinder().hashCode()
+ ", remove it");
mp.stop();
}
}
MediaProjectionEntry entry = new MediaProjectionEntry(uidpid, projection.asBinder(),
projection);
mEntryMap.put(projection.asBinder(), entry);
dispatchStart(projection);
logd("startProjectionLocked: start success, "
+ "current MediaProjection count " + mEntryMap.size());
}
private void stopProjectionLocked(final MediaProjection projection) {
logd("stopProjectionLocked: MediaProjection=" + projection.getProjectionInfo()
+ " hashCode " + projection.asBinder().hashCode());
mEntryMap.remove(projection.asBinder());
dispatchStop(projection);
logd("stopProjectionLocked: stop success, "
+ "remaining MediaProjection count " + mEntryMap.size());
}
private void addCallback(final IMediaProjectionWatcherCallback callback) {
logd("addCallback: callback=" + callback);
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
removeCallback(callback);
}
};
synchronized (mLock) {
mCallbackDelegate.add(callback);
linkDeathRecipientLocked(callback, deathRecipient);
}
}
private void removeCallback(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
logd("removeCallback: callback=" + callback);
unlinkDeathRecipientLocked(callback);
mCallbackDelegate.remove(callback);
}
}
private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback,
IBinder.DeathRecipient deathRecipient) {
try {
logd("linkDeathRecipientLocked: callback=" + callback
+ ", deathRecipient=" + deathRecipient);
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) {
logd("unlinkDeathRecipientLocked: callback=" + callback);
final IBinder token = callback.asBinder();
IBinder.DeathRecipient deathRecipient = mDeathEaters.remove(token);
if (deathRecipient != null) {
token.unlinkToDeath(deathRecipient, 0);
}
}
private void dispatchStart(MediaProjection projection) {
logd("dispatchStart: MediaProjection=" + projection.getProjectionInfo()
+ " hashCode " + projection.asBinder().hashCode());
mCallbackDelegate.dispatchStart(projection);
}
private void dispatchStop(MediaProjection projection) {
logd("dispatchStop: MediaProjection=" + projection.getProjectionInfo()
+ " hashCode " + projection.asBinder().hashCode());
mCallbackDelegate.dispatchStop(projection);
}
private boolean isValidMediaProjection(IBinder token) {
synchronized (mLock) {
logd("isValidMediaProjection: MediaProjection hashCode " + token.hashCode());
if (mEntryMap == null && mEntryMap.isEmpty()) {
return false;
}
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null && entry.getProjectionToken().equals(token)) {
logd("isValidMediaProjection: MediaProjection hashCode "
+ token.hashCode() + " is valid");
return true;
}
}
logd("isValidMediaProjection: MediaProjection hashCode "
+ token.hashCode() + " is invalid");
return false;
}
}
private MediaProjectionInfo getActiveProjectionInfo(String uidpid) {
synchronized (mLock) {
if (mEntryMap == null || mEntryMap.isEmpty()) {
return null;
}
MediaProjectionEntry entry = getMediaProjectionEntryByUidPid(uidpid);
if (entry == null) {
return null;
}
logd("getActiveProjectionInfo: MediaProjection="
+ entry.getProjectionGrant().getProjectionInfo()
+ " hashCode " + entry.getProjectionToken().hashCode());
return entry.getProjectionGrant().getProjectionInfo();
}
}
private void dump(final PrintWriter pw) {
pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
synchronized (mLock) {
pw.println("Media Projection: ");
if (mEntryMap != null && !mEntryMap.isEmpty()) {
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null) {
entry.dump(pw);
}
}
} else {
pw.println("null");
}
}
}
private final class BinderService extends IMediaProjectionManager.Stub {
@Override // Binder call
public boolean hasProjectionPermission(int uid, String packageName) {
logd("BinderService hasProjectionPermission: uid=" + uid
+ ", packageName=" + 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) {
final String uidpid = getCallingUidPid();
logd("BinderService createProjection: uid=" + uid
+ ", packageName=" + packageName + ", " + "type=" + type
+ ", isPermanentGrant=" + isPermanentGrant + ", calling uidpid " + uidpid);
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");
}
final UserHandle callingUser = Binder.getCallingUserHandle();
long callingToken = Binder.clearCallingIdentity();
MediaProjection projection;
try {
ApplicationInfo ai;
try {
ai = mPackageManager.getApplicationInfoAsUser(packageName, 0, callingUser);
} catch (NameNotFoundException e) {
throw new IllegalArgumentException("No package matching :" + packageName);
}
projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion,
ai.isPrivilegedApp());
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() {
final String uidpid = getCallingUidPid();
logd("BinderService getActiveProjectionInfo: calling uidpid " + uidpid);
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(uidpid);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
public void stopActiveProjection() {
final String uidpid = getCallingUidPid();
logd("BinderService stopActiveProjection: calling uidpid " + uidpid);
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 {
MediaProjectionEntry entry = getMediaProjectionEntryByUidPid(uidpid);
if (entry != null) {
logd("BinderService stopActiveProjection: MediaProjection="
+ entry.getProjectionGrant().getProjectionInfo()
+ " hashCode " + entry.getProjectionToken().hashCode());
entry.getProjectionGrant().stop();
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override //Binder call
public void addCallback(final IMediaProjectionWatcherCallback callback) {
logd("BinderService addCallback: callback=" + 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) {
logd("BinderService removeCallback: callback=" + 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 (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) 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 final int mTargetSdkVersion;
private final boolean mIsPrivileged;
private final int mType;
private IMediaProjectionCallback mCallback;
private IBinder mToken;
private IBinder.DeathRecipient mDeathEater;
private boolean mRestoreSystemAlertWindow;
MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
boolean isPrivileged) {
mType = type;
this.uid = uid;
this.packageName = packageName;
userHandle = new UserHandle(UserHandle.getUserId(uid));
mTargetSdkVersion = targetSdkVersion;
mIsPrivileged = isPrivileged;
}
@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
|| mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE;
}
@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) {
final String uidpid = getCallingUidPid();
logd("MediaProjection start: calling uidpid " + uidpid
+ ", callback=" + callback);
if (isValidMediaProjection(asBinder())) {
Slog.w(TAG, "UID " + Binder.getCallingUid()
+ " attempted to start already started MediaProjection");
return;
}
if (REQUIRE_FG_SERVICE_FOR_PROJECTION
&& requiresForegroundService()
&& !mActivityManagerInternal.hasRunningForegroundService(
uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
mCallback = callback;
registerCallback(mCallback);
try {
mToken = callback.asBinder();
mDeathEater = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mCallbackDelegate.remove(asBinder(), callback);
stop();
}
};
mToken.linkToDeath(mDeathEater, 0);
} catch (RemoteException e) {
Slog.w(TAG, "MediaProjectionCallbacks must be valid, aborting " +
"MediaProjection", e);
return;
}
if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) {
final long token = Binder.clearCallingIdentity();
try {
// We allow an app running a current screen capture session to use
// SYSTEM_ALERT_WINDOW for the duration of the session, to enable
// them to overlay their UX on top of what is being captured.
// We only do this if the app requests the permission, and the appop
// is in its default state (the user has neither explicitly allowed nor
// disallowed it).
final PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser(
packageName, PackageManager.GET_PERMISSIONS,
UserHandle.getUserId(uid));
if (ArrayUtils.contains(packageInfo.requestedPermissions,
Manifest.permission.SYSTEM_ALERT_WINDOW)) {
final int currentMode = mAppOps.unsafeCheckOpRawNoThrow(
AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName);
if (currentMode == AppOpsManager.MODE_DEFAULT) {
mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid,
packageName, AppOpsManager.MODE_ALLOWED);
mRestoreSystemAlertWindow = true;
}
}
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package not found, aborting MediaProjection", e);
return;
} finally {
Binder.restoreCallingIdentity(token);
}
}
startProjectionLocked(uidpid, this);
}
}
@Override // Binder call
public void stop() {
synchronized (mLock) {
logd("MediaProjection stop: calling uidpid " + getCallingUidPid());
if (!isValidMediaProjection(asBinder())) {
Slog.w(TAG, "Attempted to stop inactive MediaProjection "
+ "(uid=" + Binder.getCallingUid() + ", "
+ "pid=" + Binder.getCallingPid() + ")");
return;
}
if (mRestoreSystemAlertWindow) {
final long token = Binder.clearCallingIdentity();
try {
// Put the appop back how it was, unless it has been changed from what
// we set it to.
// Note that WindowManager takes care of removing any existing overlay
// windows when we do this.
final int currentMode = mAppOps.unsafeCheckOpRawNoThrow(
AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName);
if (currentMode == AppOpsManager.MODE_ALLOWED) {
mAppOps.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, uid, packageName,
AppOpsManager.MODE_DEFAULT);
}
mRestoreSystemAlertWindow = false;
} finally {
Binder.restoreCallingIdentity(token);
}
}
stopProjectionLocked(this);
mToken.unlinkToDeath(mDeathEater, 0);
mToken = null;
unregisterCallbacks();
mCallback = null;
}
}
@Override
public void registerCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
logd("MediaProjection registerCallback: MediaProjection(hashCode "
+ asBinder().hashCode() + ") callback=" + callback);
mCallbackDelegate.add(asBinder(), callback);
}
@Override
public void unregisterCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
logd("MediaProjection unregisterCallback: MediaProjection(hashCode "
+ asBinder().hashCode() + ") callback=" + callback);
mCallbackDelegate.remove(asBinder(), callback);
}
private void unregisterCallbacks() {
logd("MediaProjection unregister callbacks for MediaProjection(hashCode "
+ asBinder().hashCode() + ") because media projection stopped");
mCallbackDelegate.remove(asBinder());
}
public MediaProjectionInfo getProjectionInfo() {
return new MediaProjectionInfo(packageName, userHandle);
}
boolean requiresForegroundService() {
return mTargetSdkVersion >= Build.VERSION_CODES.Q && !mIsPrivileged;
}
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) {
logd("MediaRouterCallback onRouteSelected: router=" + router + ", type="
+ type + ", info=" + info + ", calling uidpid " + getCallingUidPid());
if ((type & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
mMediaRouteInfo = info;
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null) {
entry.getProjectionGrant().stop();
}
}
}
}
}
@Override
public void onRouteUnselected(MediaRouter route, int type, MediaRouter.RouteInfo info) {
logd("MediaRouterCallback onRouteUnselected: router=" + route
+ ", type=" + type + ", info=" + info);
if (mMediaRouteInfo == info) {
mMediaRouteInfo = null;
}
}
}
private static class CallbackDelegate {
private Map<IBinder, Map<IBinder, IMediaProjectionCallback>> mClientCallbacks;
private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks;
private Handler mHandler;
private Object mLock = new Object();
public CallbackDelegate() {
mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
mClientCallbacks = new ArrayMap<IBinder, Map<IBinder, IMediaProjectionCallback>>();
mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
}
public void add(IBinder token, IMediaProjectionCallback callback) {
synchronized (mLock) {
if (mClientCallbacks.get(token) == null) {
mClientCallbacks.put(token, new ArrayMap<IBinder, IMediaProjectionCallback>());
logd("CallbackDelegate add: create callback container for "
+ "MediaProjection(hashCode " + token.hashCode() + ")");
}
Map<IBinder, IMediaProjectionCallback> callbacks = mClientCallbacks.get(token);
callbacks.put(callback.asBinder(), callback);
logd("CallbackDelegate add callback: MediaProjection(hashCode "
+ token.hashCode() + ") callback=" + callback
+ ", remaining IMediaProjectionCallback for MediaProjection(hashCode "
+ token.hashCode() + ") count " + callbacks.size()
+ ", remaining IMediaProjectionCallback count "
+ mClientCallbacks.size());
}
}
public void add(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
mWatcherCallbacks.put(callback.asBinder(), callback);
logd("CallbackDelegate add callback " + callback.hashCode()
+ ", remaining IMediaProjectionWatcherCallback count "
+ mWatcherCallbacks.size());
}
}
public void remove(IBinder token, IMediaProjectionCallback callback) {
synchronized (mLock) {
Map<IBinder, IMediaProjectionCallback> callbacks = mClientCallbacks.get(token);
if (callbacks != null) {
callbacks.remove(callback.asBinder());
logd("CallbackDelegate remove callback: MediaProjection(hashCode "
+ token.hashCode() + ") callback=" + callback
+ ", remaining IMediaProjectionCallback for MediaProjection(hashCode "
+ token.hashCode() + ") count " + callbacks.size()
+ ", remaining IMediaProjectionCallback count "
+ mClientCallbacks.size());
if (callbacks.isEmpty()) {
mClientCallbacks.remove(token);
}
}
}
}
public void remove(IBinder token) {
synchronized (mLock) {
mClientCallbacks.remove(token);
logd("CallbackDelegate remove all callbacks for MediaProjection(hashCode "
+ token.hashCode() + ")");
}
}
public void remove(IMediaProjectionWatcherCallback callback) {
synchronized (mLock) {
mWatcherCallbacks.remove(callback.asBinder());
logd("CallbackDelegate remove callback " + callback.hashCode()
+ ", remaining IMediaProjectionWatcherCallback count "
+ mWatcherCallbacks.size());
}
}
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) {
logd("CallbackDelegate dispatchStart: projection="
+ projection.getProjectionInfo() + " hashCode "
+ projection.asBinder().hashCode()
+ ", IMediaProjectionWatcherCallback count " + mWatcherCallbacks.size());
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) {
logd("CallbackDelegate dispatchStop: projection="
+ projection.getProjectionInfo() + " hashCode "
+ projection.asBinder().hashCode()
+ ", IMediaProjectionWatcherCallback count " + mWatcherCallbacks.size()
+ ", IMediaProjectionCallback count " + mClientCallbacks.size());
Map<IBinder, IMediaProjectionCallback> callbacks =
mClientCallbacks.get(projection.asBinder());
if (callbacks != null) {
for (IMediaProjectionCallback cb : callbacks.values()) {
logd("CallbackDelegate dispatchStop: post IMediaProjectionCallback=" + cb);
mHandler.post(new ClientStopCallback(cb));
}
}
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);
}
private static final class MediaProjectionEntry {
private String mUidPid;
private IBinder mProjectionToken;
private MediaProjection mProjectionGrant;
public MediaProjectionEntry(String uidpid, IBinder token, MediaProjection mp) {
this.mUidPid = uidpid;
this.mProjectionToken = token;
this.mProjectionGrant = mp;
}
public String getUidPid() {
return mUidPid;
}
public IBinder getProjectionToken() {
return mProjectionToken;
}
public MediaProjection getProjectionGrant() {
return mProjectionGrant;
}
public void dump(PrintWriter pw) {
pw.println("(" + mProjectionGrant.packageName + ", uid_pid=" + mUidPid
+ ", token=" + mProjectionToken.hashCode() + "): "
+ typeToString(mProjectionGrant.mType));
}
}
private MediaProjectionEntry getMediaProjectionEntryByUidPid(String uidpid) {
if (mEntryMap == null || mEntryMap.isEmpty() || uidpid == null || uidpid.length() <= 0) {
return null;
}
for (MediaProjectionEntry entry : mEntryMap.values()) {
if (entry != null && uidpid.equals(entry.getUidPid())) {
return entry;
}
}
return null;
}
private String getCallingUidPid() {
int pid = Binder.getCallingPid();
int uid = Binder.getCallingUid();
return String.format("%s_%s", uid, pid);
}
private static void logd(String msg) {
logd(TAG, msg);
}
private static void loge(String msg) {
loge(TAG, msg);
}
private static void logd(String tag, String msg) {
if (DEBUG) {
Slog.d(tag, msg);
}
}
private static void loge(String tag, String msg) {
Slog.e(tag, msg);
}
}