| /* |
| * 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 static android.Manifest.permission.MANAGE_MEDIA_PROJECTION; |
| import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; |
| import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; |
| import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT; |
| import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT; |
| import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; |
| import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; |
| import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK; |
| import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.view.Display.INVALID_DISPLAY; |
| |
| import android.Manifest; |
| import android.annotation.EnforcePermission; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.ActivityManagerInternal; |
| import android.app.AppOpsManager; |
| import android.app.IProcessObserver; |
| import android.app.compat.CompatChanges; |
| import android.compat.annotation.ChangeId; |
| import android.compat.annotation.EnabledSince; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.ApplicationInfoFlags; |
| 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.media.projection.ReviewGrantedConsentResult; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.PermissionEnforcer; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| import android.util.Slog; |
| import android.view.ContentRecordingSession; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| 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 com.android.server.wm.WindowManagerInternal; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.time.Duration; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * 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 |
| * IMediaProjectionManager#isCurrentProjection}. |
| */ |
| 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"; |
| |
| /** |
| * Determines how to respond to an app re-using a consent token; either failing or allowing the |
| * user to re-grant consent. |
| * |
| * <p>Enabled after version 33 (Android T), so applies to target SDK of 34+ (Android U+). |
| * @hide |
| */ |
| @VisibleForTesting |
| @ChangeId |
| @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) |
| static final long MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id |
| |
| 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 Injector mInjector; |
| private final Clock mClock; |
| private final AppOpsManager mAppOps; |
| private final ActivityManagerInternal mActivityManagerInternal; |
| private final PackageManager mPackageManager; |
| private final WindowManagerInternal mWmInternal; |
| |
| private final MediaRouter mMediaRouter; |
| private final MediaRouterCallback mMediaRouterCallback; |
| private MediaRouter.RouteInfo mMediaRouteInfo; |
| |
| private IBinder mProjectionToken; |
| private MediaProjection mProjectionGrant; |
| |
| public MediaProjectionManagerService(Context context) { |
| this(context, new Injector()); |
| } |
| |
| @VisibleForTesting MediaProjectionManagerService(Context context, Injector injector) { |
| super(context); |
| mContext = context; |
| mInjector = injector; |
| mClock = injector.createClock(); |
| 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(); |
| mWmInternal = LocalServices.getService(WindowManagerInternal.class); |
| mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); |
| mMediaRouterCallback = new MediaRouterCallback(); |
| Watchdog.getInstance().addMonitor(this); |
| } |
| |
| /** Functional interface for providing time. */ |
| @VisibleForTesting |
| interface Clock { |
| /** |
| * Returns current time in milliseconds since boot, not counting time spent in deep sleep. |
| */ |
| long uptimeMillis(); |
| } |
| |
| @VisibleForTesting |
| static class Injector { |
| |
| /** |
| * Returns whether we should prevent the calling app from re-using the user's consent, or |
| * allow the user to re-grant access to the same consent token. |
| */ |
| boolean shouldMediaProjectionPreventReusingConsent(MediaProjection projection) { |
| // TODO(b/269273190): query feature flag directly instead of injecting. |
| return CompatChanges.isChangeEnabled(MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT, |
| projection.packageName, UserHandle.getUserHandleForUid(projection.uid)); |
| } |
| |
| Clock createClock() { |
| return SystemClock::uptimeMillis; |
| } |
| } |
| |
| @Override |
| public void onStart() { |
| publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(mContext), |
| 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 onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { |
| mMediaRouter.rebindAsUser(to.getUserIdentifier()); |
| synchronized (mLock) { |
| if (mProjectionGrant != null) { |
| mProjectionGrant.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) { |
| if (mProjectionGrant == null || mProjectionGrant.uid != uid) { |
| return; |
| } |
| |
| if (!mProjectionGrant.requiresForegroundService()) { |
| return; |
| } |
| |
| if ((serviceTypes & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) != 0) { |
| return; |
| } |
| |
| mProjectionGrant.stop(); |
| } |
| } |
| |
| private void startProjectionLocked(final MediaProjection projection) { |
| if (mProjectionGrant != null) { |
| mProjectionGrant.stop(); |
| } |
| if (mMediaRouteInfo != null) { |
| mMediaRouter.getFallbackRoute().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() { |
| 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); |
| } |
| |
| /** |
| * Returns {@code true} when updating the current mirroring session on WM succeeded, and |
| * {@code false} otherwise. |
| */ |
| @VisibleForTesting |
| boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) { |
| synchronized (mLock) { |
| if (!mWmInternal.setContentRecordingSession( |
| incomingSession)) { |
| // Unable to start mirroring, so tear down this projection. |
| if (mProjectionGrant != null) { |
| mProjectionGrant.stop(); |
| } |
| return false; |
| } |
| if (mProjectionGrant != null) { |
| // Cache the session details. |
| mProjectionGrant.mSession = incomingSession; |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * Returns {@code true} when the given token matches the token of the current projection |
| * instance. Returns {@code false} otherwise. |
| */ |
| @VisibleForTesting |
| boolean isCurrentProjection(IBinder token) { |
| synchronized (mLock) { |
| if (mProjectionToken != null) { |
| return mProjectionToken.equals(token); |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Re-shows the permission dialog for the user to review consent they've already granted in |
| * the given projection instance. |
| * |
| * <p>Preconditions: |
| * <ul> |
| * <li>{@link IMediaProjection#isValid} returned false, rather than throwing an exception</li> |
| * <li>Given projection instance is the current projection instance.</li> |
| * <ul> |
| * |
| * <p>Returns immediately but waits to start recording until user has reviewed their consent. |
| */ |
| @VisibleForTesting |
| void requestConsentForInvalidProjection() { |
| synchronized (mLock) { |
| Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection."); |
| // Trigger the permission dialog again in SysUI |
| // Do not handle the result; SysUI will update us when the user has consented. |
| mContext.startActivityAsUser(buildReviewGrantedConsentIntent(), |
| UserHandle.getUserHandleForUid(mProjectionGrant.uid)); |
| } |
| } |
| |
| /** |
| * Returns an intent to re-show the consent dialog in SysUI. Should only be used for the |
| * scenario where the host app has re-used the consent token. |
| * |
| * <p>Consent dialog result handled in |
| * {@link BinderService#setUserReviewGrantedConsentResult(int)}. |
| */ |
| private Intent buildReviewGrantedConsentIntent() { |
| final String permissionDialogString = mContext.getResources().getString( |
| R.string.config_mediaProjectionPermissionDialogComponent); |
| final ComponentName mediaProjectionPermissionDialogComponent = |
| ComponentName.unflattenFromString(permissionDialogString); |
| // We can use mProjectionGrant since we already checked that it matches the given token. |
| return new Intent().setComponent(mediaProjectionPermissionDialogComponent) |
| .putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, true) |
| .putExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT, mProjectionGrant.packageName) |
| .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| } |
| |
| /** |
| * Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}. |
| * |
| * <p>Tears down session if user did not consent, or starts mirroring if user did consent. |
| */ |
| @VisibleForTesting |
| void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult, |
| @Nullable IMediaProjection projection) { |
| synchronized (mLock) { |
| final boolean consentGranted = |
| consentResult == RECORD_CONTENT_DISPLAY || consentResult == RECORD_CONTENT_TASK; |
| if (consentGranted && !isCurrentProjection( |
| projection == null ? null : projection.asBinder())) { |
| Slog.v(TAG, "Reusing token: Ignore consent result of " + consentResult + " for a " |
| + "token that isn't current"); |
| return; |
| } |
| if (mProjectionGrant == null) { |
| Slog.w(TAG, "Reusing token: Can't review consent with no ongoing projection."); |
| return; |
| } |
| if (mProjectionGrant.mSession == null |
| || !mProjectionGrant.mSession.isWaitingForConsent()) { |
| Slog.w(TAG, "Reusing token: Ignore consent result " + consentResult |
| + " if not waiting for the result."); |
| return; |
| } |
| Slog.v(TAG, "Reusing token: Handling user consent result " + consentResult); |
| switch (consentResult) { |
| case UNKNOWN: |
| case RECORD_CANCEL: |
| // Pass in null to stop mirroring. |
| setReviewedConsentSessionLocked(/* session= */ null); |
| // The grant may now be null if setting the session failed. |
| if (mProjectionGrant != null) { |
| // Always stop the projection. |
| mProjectionGrant.stop(); |
| } |
| break; |
| case RECORD_CONTENT_DISPLAY: |
| // TODO(270118861) The app may have specified a particular id in the virtual |
| // display config. However - below will always return INVALID since it checks |
| // that window manager mirroring is not enabled (it is always enabled for MP). |
| setReviewedConsentSessionLocked(ContentRecordingSession.createDisplaySession( |
| DEFAULT_DISPLAY)); |
| break; |
| case RECORD_CONTENT_TASK: |
| setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession( |
| mProjectionGrant.getLaunchCookie())); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Updates the session after the user has reviewed consent. There must be a current session. |
| * |
| * @param session The new session details, or {@code null} to stop recording. |
| */ |
| private void setReviewedConsentSessionLocked(@Nullable ContentRecordingSession session) { |
| if (session != null) { |
| session.setWaitingForConsent(false); |
| session.setVirtualDisplayId(mProjectionGrant.mVirtualDisplayId); |
| } |
| |
| Slog.v(TAG, "Reusing token: Processed consent so set the session " + session); |
| if (!setContentRecordingSession(session)) { |
| Slog.e(TAG, "Reusing token: Failed to set session for reused consent, so stop"); |
| // Do not need to invoke stop; updating the session does it for us. |
| } |
| } |
| |
| // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere. |
| @VisibleForTesting |
| MediaProjection createProjectionInternal(int uid, String packageName, int type, |
| boolean isPermanentGrant, UserHandle callingUser) { |
| MediaProjection projection; |
| ApplicationInfo ai; |
| try { |
| ai = mPackageManager.getApplicationInfoAsUser(packageName, ApplicationInfoFlags.of(0), |
| callingUser); |
| } catch (NameNotFoundException e) { |
| throw new IllegalArgumentException("No package matching :" + packageName); |
| } |
| final long callingToken = Binder.clearCallingIdentity(); |
| try { |
| 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; |
| } |
| |
| // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere. |
| @VisibleForTesting |
| MediaProjection getProjectionInternal(int uid, String packageName) { |
| final long callingToken = Binder.clearCallingIdentity(); |
| try { |
| // Supposedly the package has re-used the user's consent; confirm the provided details |
| // against the current projection token before re-using the current projection. |
| if (mProjectionGrant == null || mProjectionGrant.mSession == null |
| || !mProjectionGrant.mSession.isWaitingForConsent()) { |
| Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " |
| + "instance"); |
| return null; |
| } |
| // The package matches, go ahead and re-use the token for this request. |
| if (mProjectionGrant.uid == uid |
| && Objects.equals(mProjectionGrant.packageName, packageName)) { |
| Slog.v(TAG, "Reusing token: getProjection can reuse the current projection"); |
| return mProjectionGrant; |
| } else { |
| Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " |
| + "instance due to package details mismatching"); |
| return null; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingToken); |
| } |
| } |
| |
| @VisibleForTesting |
| 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 { |
| |
| BinderService(Context context) { |
| super(PermissionEnforcer.fromContext(context)); |
| } |
| |
| @Override // Binder call |
| public boolean hasProjectionPermission(int uid, String packageName) { |
| final 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(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(); |
| return createProjectionInternal(uid, packageName, type, isPermanentGrant, |
| callingUser); |
| } |
| |
| @Override // Binder call |
| @EnforcePermission(MANAGE_MEDIA_PROJECTION) |
| public IMediaProjection getProjection(int uid, String packageName) { |
| getProjection_enforcePermission(); |
| if (packageName == null || packageName.isEmpty()) { |
| throw new IllegalArgumentException("package name must not be empty"); |
| } |
| |
| MediaProjection projection; |
| final long callingToken = Binder.clearCallingIdentity(); |
| try { |
| projection = getProjectionInternal(uid, packageName); |
| } finally { |
| Binder.restoreCallingIdentity(callingToken); |
| } |
| return projection; |
| } |
| |
| @Override // Binder call |
| public boolean isCurrentProjection(IMediaProjection projection) { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to check " |
| + "if the given projection is current."); |
| } |
| return MediaProjectionManagerService.this.isCurrentProjection( |
| projection == null ? null : projection.asBinder()); |
| } |
| |
| @Override // Binder call |
| public MediaProjectionInfo getActiveProjectionInfo() { |
| if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to get " |
| + "active projection info"); |
| } |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| return MediaProjectionManagerService.this.getActiveProjectionInfo(); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @Override // Binder call |
| public void stopActiveProjection() { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to stop " |
| + "the active projection"); |
| } |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (mProjectionGrant != null) { |
| mProjectionGrant.stop(); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @Override // Binder call |
| public void notifyActiveProjectionCapturedContentResized(int width, int height) { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " |
| + "on captured content resize"); |
| } |
| if (!isCurrentProjection(mProjectionGrant)) { |
| return; |
| } |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (mProjectionGrant != null && mCallbackDelegate != null) { |
| mCallbackDelegate.dispatchResize(mProjectionGrant, width, height); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @Override |
| public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " |
| + "on captured content visibility changed"); |
| } |
| if (!isCurrentProjection(mProjectionGrant)) { |
| return; |
| } |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (mProjectionGrant != null && mCallbackDelegate != null) { |
| mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @Override //Binder call |
| public void addCallback(final IMediaProjectionWatcherCallback callback) { |
| if (mContext.checkCallingPermission(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(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 |
| public boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession, |
| @NonNull IMediaProjection projection) { |
| if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session " |
| + "details."); |
| } |
| if (!isCurrentProjection(projection)) { |
| throw new SecurityException("Unable to set ContentRecordingSession on " |
| + "non-current MediaProjection"); |
| } |
| final long origId = Binder.clearCallingIdentity(); |
| try { |
| return MediaProjectionManagerService.this.setContentRecordingSession( |
| incomingSession); |
| } finally { |
| Binder.restoreCallingIdentity(origId); |
| } |
| } |
| |
| @Override |
| public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) { |
| if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" |
| + "projection is valid."); |
| } |
| if (!isCurrentProjection(projection)) { |
| Slog.v(TAG, "Reusing token: Won't request consent again for a token that " |
| + "isn't current"); |
| return; |
| } |
| |
| // Remove calling app identity before performing any privileged operations. |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| MediaProjectionManagerService.this.requestConsentForInvalidProjection(); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @Override // Binder call |
| @EnforcePermission(MANAGE_MEDIA_PROJECTION) |
| public void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult, |
| @Nullable IMediaProjection projection) { |
| setUserReviewGrantedConsentResult_enforcePermission(); |
| // Remove calling app identity before performing any privileged operations. |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| MediaProjectionManagerService.this.setUserReviewGrantedConsentResult(consentResult, |
| projection); |
| } 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; |
| } |
| } |
| |
| @VisibleForTesting |
| final class MediaProjection extends IMediaProjection.Stub { |
| // Host app has 5 minutes to begin using the token before it is invalid. |
| // Some apps show a dialog for the user to interact with (selecting recording resolution) |
| // before starting capture, but after requesting consent. |
| final long mDefaultTimeoutMs = Duration.ofMinutes(5).toMillis(); |
| // The creation timestamp in milliseconds, measured by {@link SystemClock#uptimeMillis}. |
| private final long mCreateTimeMs; |
| 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; |
| private IBinder mLaunchCookie = null; |
| |
| // Values for tracking token validity. |
| // Timeout value to compare creation time against. |
| private long mTimeoutMs = mDefaultTimeoutMs; |
| // Count of number of times IMediaProjection#start is invoked. |
| private int mCountStarts = 0; |
| // Set if MediaProjection#createVirtualDisplay has been invoked previously (it |
| // should only be called once). |
| private int mVirtualDisplayId = INVALID_DISPLAY; |
| // The associated session details already sent to WindowManager. |
| private ContentRecordingSession mSession; |
| |
| 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; |
| mCreateTimeMs = mClock.uptimeMillis(); |
| // TODO(b/267740338): Add unit test. |
| mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), |
| MEDIA_PROJECTION_TOKEN_EVENT_CREATED); |
| } |
| |
| @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 (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to apply virtual " |
| + "display 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 (isCurrentProjection(asBinder())) { |
| Slog.w(TAG, "UID " + Binder.getCallingUid() |
| + " attempted to start already started MediaProjection"); |
| // It is possible the app didn't explicitly invoke stop before trying to start |
| // again; ensure this start is counted in case they are re-using this token. |
| mCountStarts++; |
| 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(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(this); |
| // Mark this token as used when the app gets the MediaProjection instance. |
| mCountStarts++; |
| } |
| } |
| |
| @Override // Binder call |
| public void stop() { |
| synchronized (mLock) { |
| if (!isCurrentProjection(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; |
| unregisterCallback(mCallback); |
| mCallback = null; |
| // TODO(b/267740338): Add unit test. |
| mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), |
| MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED); |
| } |
| } |
| |
| @Override // Binder call |
| public void registerCallback(IMediaProjectionCallback callback) { |
| if (callback == null) { |
| throw new IllegalArgumentException("callback must not be null"); |
| } |
| mCallbackDelegate.add(callback); |
| } |
| |
| @Override // Binder call |
| public void unregisterCallback(IMediaProjectionCallback callback) { |
| if (callback == null) { |
| throw new IllegalArgumentException("callback must not be null"); |
| } |
| mCallbackDelegate.remove(callback); |
| } |
| |
| @Override // Binder call |
| public void setLaunchCookie(IBinder launchCookie) { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set launch " |
| + "cookie."); |
| } |
| mLaunchCookie = launchCookie; |
| } |
| |
| @Override // Binder call |
| public IBinder getLaunchCookie() { |
| if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to get launch " |
| + "cookie."); |
| } |
| return mLaunchCookie; |
| } |
| |
| @Override |
| public boolean isValid() { |
| if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if this" |
| + "projection is valid."); |
| } |
| synchronized (mLock) { |
| final long curMs = mClock.uptimeMillis(); |
| final boolean hasTimedOut = curMs - mCreateTimeMs > mTimeoutMs; |
| final boolean virtualDisplayCreated = mVirtualDisplayId != INVALID_DISPLAY; |
| final boolean isValid = |
| !hasTimedOut && (mCountStarts <= 1) && !virtualDisplayCreated; |
| if (isValid) { |
| return true; |
| } |
| |
| // Can safely use mProjectionGrant since we know this is the current projection. |
| if (mInjector.shouldMediaProjectionPreventReusingConsent(mProjectionGrant)) { |
| Slog.v(TAG, "Reusing token: Throw exception due to invalid projection."); |
| // Tear down projection here; necessary to ensure (among other reasons) that |
| // stop is dispatched to client and cast icon disappears from status bar. |
| mProjectionGrant.stop(); |
| throw new IllegalStateException("Don't re-use the resultData to retrieve " |
| + "the same projection instance, and don't use a token that has " |
| + "timed out. Don't take multiple captures by invoking " |
| + "MediaProjection#createVirtualDisplay multiple times on the " |
| + "same instance."); |
| } |
| return false; |
| } |
| } |
| |
| @Override |
| public void notifyVirtualDisplayCreated(int displayId) { |
| if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to notify virtual " |
| + "display created."); |
| } |
| synchronized (mLock) { |
| mVirtualDisplayId = displayId; |
| |
| // If prior session was does not have a valid display id, then update the display |
| // so recording can start. |
| if (mSession != null && mSession.getVirtualDisplayId() == INVALID_DISPLAY) { |
| Slog.v(TAG, "Virtual display now created, so update session with the virtual " |
| + "display id"); |
| mSession.setVirtualDisplayId(mVirtualDisplayId); |
| if (!setContentRecordingSession(mSession)) { |
| Slog.e(TAG, "Failed to set session for virtual display id"); |
| // Do not need to invoke stop; updating the session does it for us. |
| } |
| } |
| } |
| } |
| |
| 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) { |
| 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<IBinder, IMediaProjectionCallback> mClientCallbacks; |
| // Map from the IBinder token representing the callback, to the callback instance. |
| // Represents the callbacks registered on the client's MediaProjectionManager. |
| private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks; |
| private Handler mHandler; |
| private final Object mLock = new Object(); |
| |
| public CallbackDelegate() { |
| mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/); |
| mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>(); |
| mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>(); |
| } |
| |
| 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()) { |
| // Notify every callback the client has registered for a particular |
| // MediaProjection instance. |
| mHandler.post(new ClientStopCallback(callback)); |
| } |
| |
| for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { |
| MediaProjectionInfo info = projection.getProjectionInfo(); |
| mHandler.post(new WatcherStopCallback(info, callback)); |
| } |
| } |
| } |
| |
| public void dispatchResize(MediaProjection projection, int width, int height) { |
| if (projection == null) { |
| Slog.e(TAG, |
| "Tried to dispatch resize notification for a null media projection. " |
| + "Ignoring!"); |
| return; |
| } |
| synchronized (mLock) { |
| // TODO(b/249827847): Currently the service assumes there is only one projection |
| // at once - need to find the callback for the given projection, when there are |
| // multiple sessions. |
| for (IMediaProjectionCallback callback : mClientCallbacks.values()) { |
| mHandler.post(() -> { |
| try { |
| // Notify every callback the client has registered for a particular |
| // MediaProjection instance. |
| callback.onCapturedContentResize(width, height); |
| } catch (RemoteException e) { |
| Slog.w(TAG, "Failed to notify media projection has resized to " + width |
| + " x " + height, e); |
| } |
| }); |
| } |
| // Do not need to notify watcher callback about resize, since watcher callback |
| // is for passing along if recording is still ongoing or not. |
| } |
| } |
| |
| public void dispatchVisibilityChanged(MediaProjection projection, boolean isVisible) { |
| if (projection == null) { |
| Slog.e(TAG, |
| "Tried to dispatch visibility changed notification for a null media " |
| + "projection. Ignoring!"); |
| return; |
| } |
| synchronized (mLock) { |
| // TODO(b/249827847): Currently the service assumes there is only one projection |
| // at once - need to find the callback for the given projection, when there are |
| // multiple sessions. |
| for (IMediaProjectionCallback callback : mClientCallbacks.values()) { |
| mHandler.post(() -> { |
| try { |
| // Notify every callback the client has registered for a particular |
| // MediaProjection instance. |
| callback.onCapturedContentVisibilityChanged(isVisible); |
| } catch (RemoteException e) { |
| Slog.w(TAG, |
| "Failed to notify media projection has captured content " |
| + "visibility change to " |
| + isVisible, e); |
| } |
| }); |
| } |
| // Do not need to notify watcher callback about visibility changes, since watcher |
| // callback is for passing along if recording is still ongoing or not. |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |