Merge "[MediaProjection] Introduce resize callback."

This commit is contained in:
Naomi Musgrave
2022-12-14 22:13:55 +00:00
committed by Android (Google) Code Review
11 changed files with 241 additions and 13 deletions

View File

@@ -25464,6 +25464,7 @@ package android.media.projection {
public abstract static class MediaProjection.Callback {
ctor public MediaProjection.Callback();
method public void onCapturedContentResize(int, int);
method public void onStop();
}

View File

@@ -169,6 +169,12 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
"-1944652783": {
"message": "Unable to tell MediaProjectionManagerService to stop the active projection: %s",
"level": "ERROR",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecorder.java"
},
"-1941440781": {
"message": "Creating Pending Move-to-back: %s",
"level": "VERBOSE",
@@ -715,6 +721,12 @@
"group": "WM_DEBUG_ADD_REMOVE",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
"-1423223548": {
"message": "Unable to tell MediaProjectionManagerService about resizing the active projection: %s",
"level": "ERROR",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecorder.java"
},
"-1421296808": {
"message": "Moving to RESUMED: %s (in existing)",
"level": "VERBOSE",

View File

@@ -19,4 +19,5 @@ package android.media.projection;
/** {@hide} */
oneway interface IMediaProjectionCallback {
void onStop();
void onCapturedContentResize(int width, int height);
}

View File

@@ -27,12 +27,32 @@ import android.view.ContentRecordingSession;
interface IMediaProjectionManager {
@UnsupportedAppUsage
boolean hasProjectionPermission(int uid, String packageName);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
IMediaProjection createProjection(int uid, String packageName, int type,
boolean permanentGrant);
boolean isValidMediaProjection(IMediaProjection projection);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
MediaProjectionInfo getActiveProjectionInfo();
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void stopActiveProjection();
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void notifyActiveProjectionCapturedContentResized(int width, int height);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void addCallback(IMediaProjectionWatcherCallback callback);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void removeCallback(IMediaProjectionWatcherCallback callback);
/**

View File

@@ -234,7 +234,7 @@ public final class MediaProjection {
/**
* Callbacks for the projection session.
*/
public static abstract class Callback {
public abstract static class Callback {
/**
* Called when the MediaProjection session is no longer valid.
* <p>
@@ -243,6 +243,46 @@ public final class MediaProjection {
* </p>
*/
public void onStop() { }
/**
* Indicates the width and height of the captured region in pixels. Called immediately after
* capture begins to provide the app with accurate sizing for the stream. Also called
* when the region captured in this MediaProjection session is resized.
* <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()}
* </p>
* <p>
* Without the application resizing the {@link VirtualDisplay} (returned from
* {@code MediaProjection#createVirtualDisplay}) and output {@link Surface} (provided
* to {@code MediaProjection#createVirtualDisplay}), the captured stream will have
* letterboxing (black bars) around the recorded content to make up for the
* difference in aspect ratio.
* </p>
* <p>
* The application can prevent the letterboxing by overriding this method, and
* updating the size of both the {@link VirtualDisplay} and output {@link Surface}:
* </p>
*
* <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) { }
}
private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub {
@@ -252,6 +292,13 @@ public final class MediaProjection {
cbr.onStop();
}
}
@Override
public void onCapturedContentResize(int width, int height) {
for (CallbackRecord cbr : mCallbacks.values()) {
cbr.onCapturedContentResize(width, height);
}
}
}
private final static class CallbackRecord {
@@ -271,5 +318,9 @@ public final class MediaProjection {
}
});
}
public void onCapturedContentResize(int width, int height) {
mHandler.post(() -> mCallback.onCapturedContentResize(width, height));
}
}
}

View File

@@ -11420,6 +11420,11 @@ public class AudioService extends IAudioService.Stub
public void onStop() {
unregisterAudioPolicyAsync(mPolicyCallback);
}
@Override
public void onCapturedContentResize(int width, int height) {
// Ignore resize of the captured content.
}
};
UnregisterOnStopCallback mProjectionCallback;

View File

@@ -599,6 +599,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
handleMediaProjectionStoppedLocked(mAppToken);
}
}
@Override
public void onCapturedContentResize(int width, int height) {
// Do nothing when we tell the client that the content is resized - it is up to them
// to decide to update the VirtualDisplay and Surface.
// We could only update the VirtualDisplay size, anyway (which the client wouldn't
// expect), and there will still be letterboxing on the output content since the
// Surface and VirtualDisplay would then have different aspect ratios.
}
}
@VisibleForTesting

View File

@@ -315,7 +315,7 @@ public final class MediaProjectionManagerService extends SystemService
@Override // Binder call
public boolean isValidMediaProjection(IMediaProjection projection) {
return MediaProjectionManagerService.this.isValidMediaProjection(
projection.asBinder());
projection == null ? null : projection.asBinder());
}
@Override // Binder call
@@ -348,7 +348,26 @@ public final class MediaProjectionManagerService extends SystemService
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
public void notifyActiveProjectionCapturedContentResized(int width, int height) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+ "on captured content resize");
}
if (!isValidMediaProjection(mProjectionGrant)) {
return;
}
final long token = Binder.clearCallingIdentity();
try {
if (mProjectionGrant != null && mCallbackDelegate != null) {
mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override //Binder call
@@ -659,9 +678,11 @@ public final class MediaProjectionManagerService extends SystemService
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 Object mLock = new Object();
private final Object mLock = new Object();
public CallbackDelegate() {
mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
@@ -715,6 +736,8 @@ public final class MediaProjectionManagerService extends SystemService
}
synchronized (mLock) {
for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
// Notify every callback the client has registered for a particular
// MediaProjection instance.
mHandler.post(new ClientStopCallback(callback));
}
@@ -724,6 +747,33 @@ public final class MediaProjectionManagerService extends SystemService
}
}
}
public void dispatchResize(MediaProjection projection, int width, int height) {
if (projection == null) {
Slog.e(TAG, "Tried to dispatch stop 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.
}
}
}
private static final class WatcherStartCallback implements Runnable {

View File

@@ -16,6 +16,7 @@
package com.android.server.wm;
import static android.content.Context.MEDIA_PROJECTION_SERVICE;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
@@ -27,8 +28,10 @@ import android.annotation.Nullable;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.projection.MediaProjectionManager;
import android.media.projection.IMediaProjectionManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.DeviceConfig;
import android.view.ContentRecordingSession;
import android.view.Display;
@@ -83,13 +86,7 @@ final class ContentRecorder implements WindowContainerListener {
private int mLastOrientation = ORIENTATION_UNDEFINED;
ContentRecorder(@NonNull DisplayContent displayContent) {
this(displayContent, () -> {
MediaProjectionManager mpm = displayContent.mWmService.mContext.getSystemService(
MediaProjectionManager.class);
if (mpm != null) {
mpm.stopActiveProjection();
}
});
this(displayContent, new RemoteMediaProjectionManagerWrapper());
}
@VisibleForTesting
@@ -445,6 +442,9 @@ final class ContentRecorder implements WindowContainerListener {
.setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */)
.apply();
mLastRecordedBounds = new Rect(recordedContentBounds);
// Request to notify the client about the resize.
mMediaProjectionManager.notifyActiveProjectionCapturedContentResized(
mLastRecordedBounds.width(), mLastRecordedBounds.height());
}
/**
@@ -503,6 +503,56 @@ final class ContentRecorder implements WindowContainerListener {
@VisibleForTesting interface MediaProjectionManagerWrapper {
void stopActiveProjection();
void notifyActiveProjectionCapturedContentResized(int width, int height);
}
private static final class RemoteMediaProjectionManagerWrapper implements
MediaProjectionManagerWrapper {
@Nullable private IMediaProjectionManager mIMediaProjectionManager = null;
@Override
public void stopActiveProjection() {
fetchMediaProjectionManager();
if (mIMediaProjectionManager == null) {
return;
}
try {
mIMediaProjectionManager.stopActiveProjection();
} catch (RemoteException e) {
ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
"Unable to tell MediaProjectionManagerService to stop the active "
+ "projection: %s",
e);
}
}
@Override
public void notifyActiveProjectionCapturedContentResized(int width, int height) {
fetchMediaProjectionManager();
if (mIMediaProjectionManager == null) {
return;
}
try {
mIMediaProjectionManager.notifyActiveProjectionCapturedContentResized(width,
height);
} catch (RemoteException e) {
ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
"Unable to tell MediaProjectionManagerService about resizing the active "
+ "projection: %s",
e);
}
}
private void fetchMediaProjectionManager() {
if (mIMediaProjectionManager != null) {
return;
}
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
if (b == null) {
return;
}
mIMediaProjectionManager = IMediaProjectionManager.Stub.asInterface(b);
}
}
private boolean isRecordingContentTask() {

View File

@@ -45,6 +45,7 @@
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION"/>
<!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
<application android:debuggable="true"

View File

@@ -52,6 +52,8 @@ import android.view.SurfaceControl;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.server.wm.ContentRecorder.MediaProjectionManagerWrapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -79,7 +81,7 @@ public class ContentRecorderTests extends WindowTestsBase {
private ContentRecordingSession mTaskSession;
private static Point sSurfaceSize;
private ContentRecorder mContentRecorder;
@Mock private ContentRecorder.MediaProjectionManagerWrapper mMediaProjectionManagerWrapper;
@Mock private MediaProjectionManagerWrapper mMediaProjectionManagerWrapper;
private SurfaceControl mRecordedSurface;
// Handle feature flag.
private ConfigListener mConfigListener;
@@ -241,7 +243,7 @@ public class ContentRecorderTests extends WindowTestsBase {
}
@Test
public void testOnTaskConfigurationChanged_resizesSurface() {
public void testOnTaskOrientationConfigurationChanged_resizesSurface() {
mContentRecorder.setContentRecordingSession(mTaskSession);
mContentRecorder.updateRecording();
@@ -255,6 +257,29 @@ public class ContentRecorderTests extends WindowTestsBase {
anyFloat(), anyFloat());
}
@Test
public void testOnTaskBoundsConfigurationChanged_notifiesCallback() {
final int recordedWidth = 333;
final int recordedHeight = 999;
// WHEN a recording is ongoing.
mContentRecorder.setContentRecordingSession(mTaskSession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
// WHEN a configuration change arrives, and the recorded content is a different size.
mTask.setBounds(new Rect(0, 0, recordedWidth, recordedHeight));
mContentRecorder.onConfigurationChanged(mDefaultDisplay.getLastOrientation());
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
// THEN content in the captured DisplayArea is scaled to fit the surface size.
verify(mTransaction, atLeastOnce()).setMatrix(eq(mRecordedSurface), anyFloat(), eq(0f),
eq(0f),
anyFloat());
// THEN the resize callback is notified.
verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized(
recordedWidth, recordedHeight);
}
@Test
public void testPauseRecording_pausesRecording() {
mContentRecorder.setContentRecordingSession(mDisplaySession);
@@ -324,6 +349,9 @@ public class ContentRecorderTests extends WindowTestsBase {
int scaledWidth = Math.round((float) displayAreaBounds.width() / xScale);
int xInset = (sSurfaceSize.x - scaledWidth) / 2;
verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0);
// THEN the resize callback is notified.
verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized(
displayAreaBounds.width(), displayAreaBounds.height());
}
private static class RecordingTestToken extends Binder {