diff --git a/core/api/current.txt b/core/api/current.txt index 1b9bac5519f3b..aedad0d748f88 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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(); } diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index d4ac531b9c2dd..1ab5e4bef2bbe 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -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", diff --git a/media/java/android/media/projection/IMediaProjectionCallback.aidl b/media/java/android/media/projection/IMediaProjectionCallback.aidl index f3743d1307e93..2c8de2e4eec1d 100644 --- a/media/java/android/media/projection/IMediaProjectionCallback.aidl +++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl @@ -19,4 +19,5 @@ package android.media.projection; /** {@hide} */ oneway interface IMediaProjectionCallback { void onStop(); + void onCapturedContentResize(int width, int height); } diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 1d58a409718de..a63d02bb11103 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -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); /** diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index ae44fc575f7ce..3dfff1fbfc1b7 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -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. *

@@ -243,6 +243,46 @@ public final class MediaProjection { *

*/ 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. + *

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

+ *

+ * 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. + *

+ *

+ * The application can prevent the letterboxing by overriding this method, and + * 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) { } } 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)); + } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index fe40bd60bc017..9b433cf654b02 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -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; diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index a118b2fa37c31..7c647cf6f4aa4 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -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 diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index ed8d852ad2da1..50e1fca138776 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -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 mClientCallbacks; + // Map from the IBinder token representing the callback, to the callback instance. + // Represents the callbacks registered on the client's MediaProjectionManager. private Map 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 { diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 6e23ed966ddc1..8d5d0d5c1ce25 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -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() { diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index 107bbe1c79e3a..593ee4a7fa1a7 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -45,6 +45,7 @@ +