Merge "[MediaProjection] Introduce resize callback."
This commit is contained in:
committed by
Android (Google) Code Review
commit
f95354cff9
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,4 +19,5 @@ package android.media.projection;
|
||||
/** {@hide} */
|
||||
oneway interface IMediaProjectionCallback {
|
||||
void onStop();
|
||||
void onCapturedContentResize(int width, int height);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user