| /* |
| * 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 static android.view.Display.DEFAULT_DISPLAY; |
| |
| import static com.android.media.projection.flags.Flags.mediaProjectionConnectedDisplay; |
| |
| 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.DisplayManager.VirtualDisplayFlag; |
| import android.hardware.display.VirtualDisplay; |
| import android.hardware.display.VirtualDisplayConfig; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.UserManager; |
| 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. |
| * |
| * <p>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}. |
| * |
| * <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_REQUIRES_CALLBACK = 269849258L; // buganizer id |
| |
| private final IMediaProjection mImpl; |
| private final Context mContext; |
| private final DisplayManager mDisplayManager; |
| @NonNull |
| private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>(); |
| private final int mDisplayId; |
| |
| /** @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; |
| mDisplayManager = displayManager; |
| |
| try { |
| mImpl.start(new MediaProjectionCallback()); |
| |
| if (mediaProjectionConnectedDisplay()) { |
| int displayId = mImpl.getDisplayId(); |
| if (displayId != DEFAULT_DISPLAY) { |
| mDisplayId = displayId; |
| Log.v(TAG, "Created MediaProjection for display " + mDisplayId); |
| return; |
| } |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Content Recording: Failed to start media projection", e); |
| throw new RuntimeException("Failed to start media projection", e); |
| } |
| |
| final UserManager userManager = context.getSystemService(UserManager.class); |
| mDisplayId = userManager.isVisibleBackgroundUsersSupported() |
| ? userManager.getMainDisplayIdAssignedToUser() |
| : DEFAULT_DISPLAY; |
| } |
| |
| /** |
| * Register a listener to receive notifications about when the {@link MediaProjection} or |
| * captured content changes state. |
| * |
| * <p>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()} to |
| * properly handle MediaProjection clean up any resources it is holding, e.g. the {@link |
| * VirtualDisplay} and {@link Surface}. This should also update any application UI indicating |
| * the MediaProjection status as MediaProjection has stopped. |
| * |
| * @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(mContext.getMainLooper()); |
| } |
| 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 |
| */ |
| @Nullable |
| 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); |
| } |
| builder.setDisplayIdToMirror(mDisplayId); |
| return createVirtualDisplay(builder, callback, handler); |
| } |
| |
| /** |
| * Creates a {@link android.hardware.display.VirtualDisplay} to capture the contents of the |
| * screen. |
| * |
| * <p>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 |
| * resources (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related |
| * resources) and to update any available UI regarding the MediaProjection status. |
| * |
| * @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: |
| * <ol> |
| * <li>If attempting to create a new virtual display associated with this MediaProjection |
| * instance after it has been stopped by invoking {@link #stop()}. |
| * <li>If attempting to create a new virtual display associated with this MediaProjection |
| * instance after a {@link MediaProjection.Callback#onStop()} callback has been received |
| * due to the user or the system stopping the MediaProjection session. |
| * <li>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. |
| * <li>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. |
| * </ol> |
| * 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. |
| * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} could |
| * be created. |
| * @see VirtualDisplay |
| * @see VirtualDisplay.Callback |
| */ |
| @SuppressWarnings("RequiresPermission") |
| @Nullable |
| public VirtualDisplay createVirtualDisplay( |
| @NonNull String name, |
| int width, |
| int height, |
| int dpi, |
| @VirtualDisplayFlag 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); |
| } |
| builder.setDisplayIdToMirror(mDisplayId); |
| 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(StopReason.STOP_HOST_APP); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to stop projection", e); |
| } |
| } |
| |
| /** |
| * Stops projection. |
| * @hide |
| */ |
| public void stop(@StopReason int stopReason) { |
| try { |
| Log.d(TAG, "Content Recording: stopping projection"); |
| mImpl.stop(stopReason); |
| } 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 has been stopped and is no longer valid. |
| * |
| * <p>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}). If the application is displaying any UI indicating the MediaProjection state |
| * it should be updated to indicate that MediaProjection is no longer active. |
| * |
| * <p>MediaProjection stopping can be a result of the system stopping the ongoing |
| * MediaProjection due to various reasons, such as another MediaProjection session starting, |
| * a user stopping the session via UI affordances in system-level UI, or the screen being |
| * locked. |
| * |
| * <p>After this callback any call to {@link MediaProjection#createVirtualDisplay} will |
| * fail, even if no such {@link VirtualDisplay} was ever created for this MediaProjection |
| * session. |
| */ |
| 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. |
| * |
| * <p>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. |
| * |
| * <p>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}: |
| * |
| * <pre> |
| * @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); |
| * }</pre> |
| */ |
| 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. |
| * |
| * <p>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. |
| * |
| * <p>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: |
| * <ul> |
| * <li> |
| * The captured region is visible ({@code isVisible} with value {@code true}), |
| * because the captured app is at least partially visible. This may happen if the |
| * user moves the covering app to show at least some portion of the captured app |
| * (e.g. the user has multiple apps visible in a multi-window mode such as split |
| * screen). |
| * </li> |
| * <li> |
| * The captured region is invisible ({@code isVisible} with value {@code false}) if |
| * it is entirely hidden. This may happen if another app entirely covers the |
| * captured app, or the user navigates away from the captured app. |
| * </li> |
| * </ul> |
| */ |
| 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)); |
| } |
| } |
| } |