script-astra/Android/Sdk/sources/android-35/android/media/projection/MediaProjection.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

436 lines
20 KiB
Java

/*
* 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.
*
* <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<>();
/** @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.
*
* <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()} 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.
*
* <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 (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:
* <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 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.
* @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.
*
* <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}).
*/
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>
* &#x40;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));
}
}
}