Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 75d36c6b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "[MediaProjection] Require apps register a callback before recording"...

Merge "[MediaProjection] Require apps register a callback before recording" into udc-dev am: f9e09190

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21611649



Change-Id: I45c06e2b4db4c11d40478019397e9788f21983b1
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents a62705c2 f9e09190
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -26284,9 +26284,9 @@ package android.media.projection {
  public final class MediaProjection {
    method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
    method public void registerCallback(android.media.projection.MediaProjection.Callback, android.os.Handler);
    method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler);
    method public void stop();
    method public void unregisterCallback(android.media.projection.MediaProjection.Callback);
    method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback);
  }
  public abstract static class MediaProjection.Callback {
+121 −71
Original line number Diff line number Diff line
@@ -20,43 +20,73 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;

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.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.view.ContentRecordingSession;
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
 * <p>A screen capture session can be started through {@link
 * MediaProjectionManager#createScreenCaptureIntent}. This grants the ability to
 * capture screen contents, but not system audio.
 * </p>
 */
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 Map<Callback, CallbackRecord> mCallbacks;
    @Nullable private IMediaProjectionManager mProjectionService = null;
    private final DisplayManager mDisplayManager;
    private final IMediaProjectionManager mProjectionService;
    @NonNull
    private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>();

    /** @hide */
    public MediaProjection(Context context, IMediaProjection impl) {
        mCallbacks = new ArrayMap<Callback, CallbackRecord>();
        this(context, impl, IMediaProjectionManager.Stub.asInterface(
                        ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE)),
                context.getSystemService(DisplayManager.class));
    }

    /** @hide */
    @VisibleForTesting
    public MediaProjection(Context context, IMediaProjection impl, IMediaProjectionManager service,
            DisplayManager displayManager) {
        mContext = context;
        mImpl = impl;
        try {
@@ -64,46 +94,44 @@ public final class MediaProjection {
        } catch (RemoteException e) {
            throw new RuntimeException("Failed to start media projection", e);
        }
        mProjectionService = service;
        mDisplayManager = displayManager;
    }

    /**
     * Register a listener to receive notifications about when the {@link MediaProjection} or
     * captured content changes state.
     * <p>
     * The callback should be registered before invoking
     *
     * <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.
     * </p>
     * 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 IllegalArgumentException If the given callback is null.
     * @throws NullPointerException If the given callback is null.
     * @see #unregisterCallback
     */
    public void registerCallback(Callback callback, Handler handler) {
        if (callback == null) {
            throw new IllegalArgumentException("callback should not be null");
        }
    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
        final Callback c = Objects.requireNonNull(callback);
        if (handler == null) {
            handler = new Handler();
        }
        mCallbacks.put(callback, new CallbackRecord(callback, handler));
        mCallbacks.put(c, new CallbackRecord(c, handler));
    }

    /**
     * Unregister a {@link MediaProjection} listener.
     *
     * @param callback The callback to unregister.
     * @throws IllegalArgumentException If the given callback is null.
     * @throws NullPointerException If the given callback is null.
     * @see #registerCallback
     */
    public void unregisterCallback(Callback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback should not be null");
        }
        mCallbacks.remove(callback);
    public void unregisterCallback(@NonNull Callback callback) {
        final Callback c = Objects.requireNonNull(callback);
        mCallbacks.remove(c);
    }

    /**
@@ -122,43 +150,55 @@ public final class MediaProjection {
        if (surface != null) {
            builder.setSurface(surface);
        }
        VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler);
        return virtualDisplay;
        return createVirtualDisplay(builder, callback, handler);
    }

    /**
     * Creates a {@link android.hardware.display.VirtualDisplay} to capture the
     * contents of the screen.
     *
     * @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.
     * @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}.
     * <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).
     *
     * @see android.hardware.display.VirtualDisplay
     * @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.
     * @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 no {@link Callback}
     *                               is registered. If the target SDK is less than
     *                               {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}, no
     *                               exception is thrown.
     * @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()) {
                throw new IllegalStateException(
                        "Must register a callback before starting capture, to manage resources in"
                                + " response to MediaProjection states.");
            }
        }
        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
                height, dpi).setFlags(flags);
        if (surface != null) {
            builder.setSurface(surface);
        }
        VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler);
        return virtualDisplay;
        return createVirtualDisplay(builder, callback, handler);
    }

    /**
@@ -191,13 +231,21 @@ public final class MediaProjection {
            } else {
                session = ContentRecordingSession.createTaskSession(launchCookie);
            }
            // Pass in the current session details, so they are guaranteed to only be set in WMS
            // AFTER a VirtualDisplay is constructed (assuming there are no errors during set-up).
            // 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).
            virtualDisplayConfig.setContentRecordingSession(session);
            virtualDisplayConfig.setWindowManagerMirroringEnabled(true);
            final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
            final VirtualDisplay virtualDisplay = dm.createVirtualDisplay(this,
            final VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(this,
                    virtualDisplayConfig.build(), callback, handler, windowContext);
            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;
        } catch (RemoteException e) {
            // Can not capture if WMS is not accessible, so bail out.
@@ -205,12 +253,14 @@ public final class MediaProjection {
        }
    }

    private IMediaProjectionManager getProjectionService() {
        if (mProjectionService == null) {
            mProjectionService = IMediaProjectionManager.Stub.asInterface(
                    ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE));
        }
        return mProjectionService;
    /**
     * 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);
    }

    /**
@@ -238,28 +288,26 @@ public final class MediaProjection {
    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. {@link android.hardware.display.VirtualDisplay}s).
         * </p>
         *
         * <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
         *
         * <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>
         * <p>
         * If the recorded content has a different aspect ratio from either the
         *
         * <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}:
         * </p>
         *
         * <pre>
         * &#x40;Override
@@ -284,13 +332,12 @@ public final class MediaProjection {
        /**
         * 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
         *
         * <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>
         * <p>
         * For example, if the user elected to capture a single app (from the activity shown from
         *
         * <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>
@@ -307,7 +354,6 @@ public final class MediaProjection {
         *         captured app, or the user navigates away from the captured app.
         *     </li>
         * </ul>
         * </p>
         */
        public void onCapturedContentVisibilityChanged(boolean isVisible) { }
    }
@@ -335,7 +381,7 @@ public final class MediaProjection {
        }
    }

    private final static class CallbackRecord {
    private static final class CallbackRecord extends Callback {
        private final Callback mCallback;
        private final Handler mHandler;

@@ -344,6 +390,8 @@ public final class MediaProjection {
            mHandler = handler;
        }


        @Override
        public void onStop() {
            mHandler.post(new Runnable() {
                @Override
@@ -353,10 +401,12 @@ public final class MediaProjection {
            });
        }

        @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));
        }
+2 −0
Original line number Diff line number Diff line
@@ -29,7 +29,9 @@ android_test {
        "mockito-target-extended-minus-junit4",
        "platform-test-annotations",
        "testng",
        "testables",
        "truth-prebuilt",
        "platform-compat-test-rules",
    ],

    // Needed for mockito-target-extended-minus-junit4
+1 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
          package="android.media.projection.mediaprojectiontests"
          android:sharedUserId="com.android.uid.test">
    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
    <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />

    <application android:debuggable="true"
                 android:testOnly="true">
+82 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.os.IBinder;
import android.os.RemoteException;

/**
 * The connection between MediaProjection and system server is represented by IMediaProjection;
 * outside the test it is implemented by the system server.
 */
public final class FakeIMediaProjection extends IMediaProjection.Stub {
    boolean mIsStarted = false;
    IBinder mLaunchCookie = null;
    IMediaProjectionCallback mIMediaProjectionCallback = null;

    @Override
    public void start(IMediaProjectionCallback callback) throws RemoteException {
        mIMediaProjectionCallback = callback;
        mIsStarted = true;
    }

    @Override
    public void stop() throws RemoteException {
        // Pass along to the client's callback wrapper.
        mIMediaProjectionCallback.onStop();
    }

    @Override
    public boolean canProjectAudio() throws RemoteException {
        return false;
    }

    @Override
    public boolean canProjectVideo() throws RemoteException {
        return false;
    }

    @Override
    public boolean canProjectSecureVideo() throws RemoteException {
        return false;
    }

    @Override
    public int applyVirtualDisplayFlags(int flags) throws RemoteException {
        return 0;
    }

    @Override
    public void registerCallback(IMediaProjectionCallback callback) throws RemoteException {

    }

    @Override
    public void unregisterCallback(IMediaProjectionCallback callback) throws RemoteException {

    }

    @Override
    public IBinder getLaunchCookie() throws RemoteException {
        return mLaunchCookie;
    }

    @Override
    public void setLaunchCookie(IBinder launchCookie) throws RemoteException {
        mLaunchCookie = launchCookie;
    }
}
Loading