/* * 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 android.media.projection; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Build; import android.os.Handler; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import java.util.Map; import java.util.Objects; /** * A token granting applications the ability to capture screen contents and/or * record system audio. The exact capabilities granted depend on the type of * MediaProjection. * *

A screen capture session can be started through {@link * MediaProjectionManager#createScreenCaptureIntent}. This grants the ability to * capture screen contents, but not system audio. */ public final class MediaProjection { private static final String TAG = "MediaProjection"; /** * Requires an app registers a {@link Callback} before invoking * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, * Handler) createVirtualDisplay}. * *

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_REQUIRES_CALLBACK = 269849258L; // buganizer id private final IMediaProjection mImpl; private final Context mContext; private final DisplayManager mDisplayManager; @NonNull private final Map mCallbacks = new ArrayMap<>(); /** @hide */ public MediaProjection(Context context, IMediaProjection impl) { this(context, impl, context.getSystemService(DisplayManager.class)); } /** @hide */ @VisibleForTesting public MediaProjection(Context context, IMediaProjection impl, DisplayManager displayManager) { mContext = context; mImpl = impl; try { mImpl.start(new MediaProjectionCallback()); } catch (RemoteException e) { Log.e(TAG, "Content Recording: Failed to start media projection", e); throw new RuntimeException("Failed to start media projection", e); } mDisplayManager = displayManager; } /** * Register a listener to receive notifications about when the {@link MediaProjection} or * captured content changes state. * *

The callback must be registered before invoking * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, * Handler)} to ensure that any notifications on the callback are not missed. The client must * implement {@link Callback#onStop()} and clean up any resources it is holding, e.g. the * {@link VirtualDisplay} and {@link Surface}. * * @param callback The callback to call. * @param handler The handler on which the callback should be invoked, or * null if the callback should be invoked on the calling thread's looper. * @throws NullPointerException If the given callback is null. * @see #unregisterCallback */ public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { try { final Callback c = Objects.requireNonNull(callback); if (handler == null) { handler = new Handler(); } mCallbacks.put(c, new CallbackRecord(c, handler)); } catch (NullPointerException e) { Log.e(TAG, "Content Recording: cannot register null Callback", e); throw e; } catch (RuntimeException e) { Log.e(TAG, "Content Recording: failed to create new Handler to register Callback", e); } } /** * Unregister a {@link MediaProjection} listener. * * @param callback The callback to unregister. * @throws NullPointerException If the given callback is null. * @see #registerCallback */ public void unregisterCallback(@NonNull Callback callback) { try { final Callback c = Objects.requireNonNull(callback); mCallbacks.remove(c); } catch (NullPointerException e) { Log.d(TAG, "Content Recording: cannot unregister null Callback", e); throw e; } } /** * @hide */ public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; if (isSecure) { flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE; } final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, height, dpi).setFlags(flags); if (surface != null) { builder.setSurface(surface); } return createVirtualDisplay(builder, callback, handler); } /** * Creates a {@link android.hardware.display.VirtualDisplay} to capture the * contents of the screen. * *

To correctly clean up resources associated with a capture, the application must register a * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean * up (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related * resources). * * @param name The name of the virtual display, must be non-empty. * @param width The width of the virtual display in pixels. Must be greater than 0. * @param height The height of the virtual display in pixels. Must be greater than 0. * @param dpi The density of the virtual display in dpi. Must be greater than 0. * @param surface The surface to which the content of the virtual display should be rendered, * or null if there is none initially. * @param flags A combination of virtual display flags. See {@link DisplayManager} for the * full list of flags. Note that * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION} * is always enabled. The following flags may be overridden, depending on how * the component with {android.Manifest.permission.MANAGE_MEDIA_PROJECTION} * handles the user's consent: * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY}, * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR}, * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC}. * @param callback Callback invoked when the virtual display's state changes, or null. * @param handler The {@link android.os.Handler} on which the callback should be invoked, or * null if the callback should be invoked on the calling thread's main * {@link android.os.Looper}. * @throws IllegalStateException If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, and * if no {@link Callback} is registered. * @throws SecurityException In any of the following scenarios: *

    *
  1. If attempting to create a new virtual display * associated with this MediaProjection instance after it has * been stopped by invoking {@link #stop()}. *
  2. If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, * and if this instance has already taken a recording through * {@code #createVirtualDisplay}, but {@link #stop()} wasn't * invoked to end the recording. *
  3. If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, * and if {@link MediaProjectionManager#getMediaProjection} * was invoked more than once to get this * {@code MediaProjection} instance. *
* In cases 2 & 3, no exception is thrown if the target SDK is * less than * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. * Instead, recording doesn't begin until the user re-grants * consent in the dialog. * @see VirtualDisplay * @see VirtualDisplay.Callback */ public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, int flags, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { if (shouldMediaProjectionRequireCallback()) { if (mCallbacks.isEmpty()) { final IllegalStateException e = new IllegalStateException( "Must register a callback before starting capture, to manage resources in" + " response to MediaProjection states."); Log.e(TAG, "Content Recording: no callback registered for virtual display", e); throw e; } } final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, height, dpi).setFlags(flags); if (surface != null) { builder.setSurface(surface); } return createVirtualDisplay(builder, callback, handler); } /** * Creates a {@link android.hardware.display.VirtualDisplay} to capture the * contents of the screen. * * @param virtualDisplayConfig The arguments for the virtual display configuration. See * {@link VirtualDisplayConfig} for using it. * @param callback Callback to call when the virtual display's state changes, or null if none. * @param handler The {@link android.os.Handler} on which the callback should be invoked, or * null if the callback should be invoked on the calling thread's main * {@link android.os.Looper}. * * @see android.hardware.display.VirtualDisplay * @hide */ @Nullable public VirtualDisplay createVirtualDisplay( @NonNull VirtualDisplayConfig.Builder virtualDisplayConfig, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { // Pass in the current session details, so they are guaranteed to only be set in // WindowManagerService AFTER a VirtualDisplay is constructed (assuming there are no // errors during set-up). // Do not introduce a separate aidl call here to prevent a race // condition between setting up the VirtualDisplay and checking token validity. virtualDisplayConfig.setWindowManagerMirroringEnabled(true); // Do not declare a display id to mirror; default to the default display. // DisplayManagerService will ask MediaProjectionManagerService to check if the app // is re-using consent. Always return the projection instance to keep this call // non-blocking; no content is sent to the app until the user re-grants consent. final VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(this, virtualDisplayConfig.build(), callback, handler); if (virtualDisplay == null) { // Since WindowManager handling a new display and DisplayManager creating a new // VirtualDisplay is async, WindowManager may have tried to start task recording // and encountered an error that required stopping recording entirely. The // VirtualDisplay would then be null and the MediaProjection is no longer active. Slog.w(TAG, "Failed to create virtual display."); return null; } return virtualDisplay; } /** * Returns {@code true} when MediaProjection requires the app registers a callback before * beginning to capture via * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, * Handler)}. */ private boolean shouldMediaProjectionRequireCallback() { return CompatChanges.isChangeEnabled(MEDIA_PROJECTION_REQUIRES_CALLBACK); } /** * Stops projection. */ public void stop() { try { Log.d(TAG, "Content Recording: stopping projection"); mImpl.stop(); } catch (RemoteException e) { Log.e(TAG, "Unable to stop projection", e); } } /** * Get the underlying IMediaProjection. * @hide */ public IMediaProjection getProjection() { return mImpl; } /** * Callbacks for the projection session. */ public abstract static class Callback { /** * Called when the MediaProjection session is no longer valid. * *

Once a MediaProjection has been stopped, it's up to the application to release any * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and * {@link Surface}). */ public void onStop() { } /** * Invoked immediately after capture begins or when the size of the captured region changes, * providing the accurate sizing for the streamed capture. * *

The given width and height, in pixels, corresponds to the same width and height that * would be returned from {@link android.view.WindowMetrics#getBounds()} of the captured * region. * *

If the recorded content has a different aspect ratio from either the * {@link VirtualDisplay} or output {@link Surface}, the captured stream has letterboxing * (black bars) around the recorded content. The application can avoid the letterboxing * around the recorded content by updating the size of both the {@link VirtualDisplay} and * output {@link Surface}: * *

         * @Override
         * public String onCapturedContentResize(int width, int height) {
         *     // VirtualDisplay instance from MediaProjection#createVirtualDisplay
         *     virtualDisplay.resize(width, height, dpi);
         *
         *     // Create a new Surface with the updated size (depending on the application's use
         *     // case, this may be through different APIs - see Surface documentation for
         *     // options).
         *     int texName; // the OpenGL texture object name
         *     SurfaceTexture surfaceTexture = new SurfaceTexture(texName);
         *     surfaceTexture.setDefaultBufferSize(width, height);
         *     Surface surface = new Surface(surfaceTexture);
         *
         *     // Ensure the VirtualDisplay has the updated Surface to send the capture to.
         *     virtualDisplay.setSurface(surface);
         * }
*/ public void onCapturedContentResize(int width, int height) { } /** * Invoked immediately after capture begins or when the visibility of the captured region * changes, providing the current visibility of the captured region. * *

Applications can take advantage of this callback by showing or hiding the captured * content from the output {@link Surface}, based on if the captured region is currently * visible to the user. * *

For example, if the user elected to capture a single app (from the activity shown from * {@link MediaProjectionManager#createScreenCaptureIntent()}), the following scenarios * trigger the callback: *

*/ public void onCapturedContentVisibilityChanged(boolean isVisible) { } } private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub { @Override public void onStop() { Slog.v(TAG, "Dispatch stop to " + mCallbacks.size() + " callbacks."); for (CallbackRecord cbr : mCallbacks.values()) { cbr.onStop(); } } @Override public void onCapturedContentResize(int width, int height) { for (CallbackRecord cbr : mCallbacks.values()) { cbr.onCapturedContentResize(width, height); } } @Override public void onCapturedContentVisibilityChanged(boolean isVisible) { for (CallbackRecord cbr : mCallbacks.values()) { cbr.onCapturedContentVisibilityChanged(isVisible); } } } private static final class CallbackRecord extends Callback { private final Callback mCallback; private final Handler mHandler; public CallbackRecord(Callback callback, Handler handler) { mCallback = callback; mHandler = handler; } @Override public void onStop() { mHandler.post(new Runnable() { @Override public void run() { mCallback.onStop(); } }); } @Override public void onCapturedContentResize(int width, int height) { mHandler.post(() -> mCallback.onCapturedContentResize(width, height)); } @Override public void onCapturedContentVisibilityChanged(boolean isVisible) { mHandler.post(() -> mCallback.onCapturedContentVisibilityChanged(isVisible)); } } }