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

Commit 414f6607 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Make ScreenshotRequest handle hardware bitmap conversion

Currently, ScreenshotHelper provides utility functions to convert
between a hardware bitmap and a parcelable form for sending between
processes, but the originating code must handle the conversion itself.

This change moves the conversion so that it occurs seamlessly
inside of the ScreenshotRequest class, and refactors the inputs
to ScreenshotHelper slightly so that the ScreenshotRequest data class
is what gets passed in.

Bug: 264457397
Test: atest
Change-Id: I1041bbcfdc5f5ece6e98bd0844017d4f3dc4e9f3
parent 08a60d28
Loading
Loading
Loading
Loading
+32 −265
Original line number Diff line number Diff line
package com.android.internal.util;

import static android.content.Intent.ACTION_USER_SWITCHED;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;

import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -11,29 +11,18 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.Insets;
import android.graphics.ParcelableColorSpace;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.WindowManager.ScreenshotSource;
import android.view.WindowManager.ScreenshotType;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Objects;
import java.util.function.Consumer;

public class ScreenshotHelper {
@@ -41,212 +30,6 @@ public class ScreenshotHelper {
    public static final int SCREENSHOT_MSG_URI = 1;
    public static final int SCREENSHOT_MSG_PROCESS_COMPLETE = 2;

    /**
     * Describes a screenshot request.
     */
    public static class ScreenshotRequest implements Parcelable {
        @ScreenshotType
        private final int mType;

        @ScreenshotSource
        private final int mSource;

        private final Bundle mBitmapBundle;
        private final Rect mBoundsInScreen;
        private final Insets mInsets;
        private final int mTaskId;
        private final int mUserId;
        private final ComponentName mTopComponent;


        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source) {
            this(type, source, /* topComponent */ null);
        }

        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
                ComponentName topComponent) {
            this(type,
                source,
                /* bitmapBundle*/ null,
                /* boundsInScreen */ null,
                /* insets */ null,
                /* taskId */ -1,
                /* userId */ -1,
                topComponent);
        }

        public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
                Bundle bitmapBundle, Rect boundsInScreen, Insets insets, int taskId, int userId,
                ComponentName topComponent) {
            mType = type;
            mSource = source;
            mBitmapBundle = bitmapBundle;
            mBoundsInScreen = boundsInScreen;
            mInsets = insets;
            mTaskId = taskId;
            mUserId = userId;
            mTopComponent = topComponent;
        }

        ScreenshotRequest(Parcel in) {
            mType = in.readInt();
            mSource = in.readInt();
            if (in.readInt() == 1) {
                mBitmapBundle = in.readBundle(getClass().getClassLoader());
                mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), Rect.class);
                mInsets = in.readParcelable(Insets.class.getClassLoader(), Insets.class);
                mTaskId = in.readInt();
                mUserId = in.readInt();
                mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(),
                        ComponentName.class);
            } else {
                mBitmapBundle = null;
                mBoundsInScreen = null;
                mInsets = null;
                mTaskId = -1;
                mUserId = -1;
                mTopComponent = null;
            }
        }

        @ScreenshotType
        public int getType() {
            return mType;
        }

        @ScreenshotSource
        public int getSource() {
            return mSource;
        }

        public Bundle getBitmapBundle() {
            return mBitmapBundle;
        }

        public Rect getBoundsInScreen() {
            return mBoundsInScreen;
        }

        public Insets getInsets() {
            return mInsets;
        }

        public int getTaskId() {
            return mTaskId;
        }

        public int getUserId() {
            return mUserId;
        }

        public ComponentName getTopComponent() {
            return mTopComponent;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mType);
            dest.writeInt(mSource);
            if (mBitmapBundle == null) {
                dest.writeInt(0);
            } else {
                dest.writeInt(1);
                dest.writeBundle(mBitmapBundle);
                dest.writeParcelable(mBoundsInScreen, 0);
                dest.writeParcelable(mInsets, 0);
                dest.writeInt(mTaskId);
                dest.writeInt(mUserId);
                dest.writeParcelable(mTopComponent, 0);
            }
        }

        @NonNull
        public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
                new Parcelable.Creator<ScreenshotRequest>() {

                    @Override
                    public ScreenshotRequest createFromParcel(Parcel source) {
                        return new ScreenshotRequest(source);
                    }

                    @Override
                    public ScreenshotRequest[] newArray(int size) {
                        return new ScreenshotRequest[size];
                    }
                };
    }

    /**
     * Bundler used to convert between a hardware bitmap and a bundle without copying the internal
     * content. This is expected to be used together with {@link #provideScreenshot} to handle a
     * hardware bitmap as a screenshot.
     */
    public static final class HardwareBitmapBundler {
        private static final String KEY_BUFFER = "bitmap_util_buffer";
        private static final String KEY_COLOR_SPACE = "bitmap_util_color_space";

        private HardwareBitmapBundler() {
        }

        /**
         * Creates a Bundle that represents the given Bitmap.
         * <p>The Bundle will contain a wrapped version of the Bitmaps HardwareBuffer, so will avoid
         * copies when passing across processes, only pass to processes you trust.
         *
         * <p>Returns a new Bundle rather than modifying an exiting one to avoid key collisions, the
         * returned Bundle should be treated as a standalone object.
         *
         * @param bitmap to convert to bundle
         * @return a Bundle representing the bitmap, should only be parsed by
         * {@link #bundleToHardwareBitmap(Bundle)}
         */
        public static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
            if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
                throw new IllegalArgumentException(
                        "Passed bitmap must have hardware config, found: " + bitmap.getConfig());
            }

            // Bitmap assumes SRGB for null color space
            ParcelableColorSpace colorSpace =
                    bitmap.getColorSpace() == null
                            ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
                            : new ParcelableColorSpace(bitmap.getColorSpace());

            Bundle bundle = new Bundle();
            bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());
            bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);

            return bundle;
        }

        /**
         * Extracts the Bitmap added to a Bundle with {@link #hardwareBitmapToBundle(Bitmap)} .}
         *
         * <p>This Bitmap contains the HardwareBuffer from the original caller, be careful passing
         * this Bitmap on to any other source.
         *
         * @param bundle containing the bitmap
         * @return a hardware Bitmap
         */
        public static Bitmap bundleToHardwareBitmap(Bundle bundle) {
            if (!bundle.containsKey(KEY_BUFFER) || !bundle.containsKey(KEY_COLOR_SPACE)) {
                throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
            }

            HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
            ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
                    ParcelableColorSpace.class);

            return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
                    colorSpace.getColorSpace());
        }
    }

    private static final String TAG = "ScreenshotHelper";

    // Time until we give up on the screenshot & show an error instead.
@@ -277,68 +60,53 @@ public class ScreenshotHelper {
    /**
     * Request a screenshot be taken.
     * <p>
     * Added to support reducing unit test duration; the method variant without a timeout argument
     * is recommended for general use.
     * Convenience method for taking a full screenshot with provided source.
     *
     * @param type The type of screenshot, defined by {@link ScreenshotType}
     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
     * @param source             source of the screenshot request, defined by {@link
     *                           ScreenshotSource}
     * @param handler            used to process messages received from the screenshot service
     * @param completionConsumer receives the URI of the captured screenshot, once saved or
     *                           null if no screenshot was saved
     */
    public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
            @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
        ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
        takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS,
                completionConsumer);
    public void takeScreenshot(@ScreenshotSource int source, @NonNull Handler handler,
            @Nullable Consumer<Uri> completionConsumer) {
        ScreenshotRequest request =
                new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, source).build();
        takeScreenshot(request, handler, completionConsumer);
    }

    /**
     * Request a screenshot be taken.
     * <p>
     * Added to support reducing unit test duration; the method variant without a timeout argument
     * is recommended for general use.
     *
     * @param type The type of screenshot, defined by {@link ScreenshotType}
     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
     * @param request            description of the screenshot request, either for taking a
     *                           screenshot or
     *                           providing a bitmap
     * @param handler            used to process messages received from the screenshot service
     * @param timeoutMs time limit for processing, intended only for testing
     * @param completionConsumer receives the URI of the captured screenshot, once saved or
     *                           null if no screenshot was saved
     */
    @VisibleForTesting
    public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
            @NonNull Handler handler, long timeoutMs, @Nullable Consumer<Uri> completionConsumer) {
        ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
        takeScreenshot(handler, screenshotRequest, timeoutMs, completionConsumer);
    public void takeScreenshot(ScreenshotRequest request, @NonNull Handler handler,
            @Nullable Consumer<Uri> completionConsumer) {
        takeScreenshotInternal(request, handler, completionConsumer, SCREENSHOT_TIMEOUT_MS);
    }

    /**
     * Request that provided image be handled as if it was a screenshot.
     * Request a screenshot be taken.
     * <p>
     * Added to support reducing unit test duration; the method variant without a timeout argument
     * is recommended for general use.
     *
     * @param screenshotBundle Bundle containing the buffer and color space of the screenshot.
     * @param boundsInScreen The bounds in screen coordinates that the bitmap originated from.
     * @param insets The insets that the image was shown with, inside the screen bounds.
     * @param taskId The taskId of the task that the screen shot was taken of.
     * @param userId The userId of user running the task provided in taskId.
     * @param topComponent The component name of the top component running in the task.
     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
     * @param handler A handler used in case the screenshot times out
     * @param request            description of the screenshot request, either for taking a
     *                           screenshot or providing a bitmap
     * @param handler            used to process messages received from the screenshot service
     * @param timeoutMs          time limit for processing, intended only for testing
     * @param completionConsumer receives the URI of the captured screenshot, once saved or
     *                           null if no screenshot was saved
     */
    public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen,
            @NonNull Insets insets, int taskId, int userId, ComponentName topComponent,
            @ScreenshotSource int source, @NonNull Handler handler,
            @Nullable Consumer<Uri> completionConsumer) {
        ScreenshotRequest screenshotRequest = new ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE,
                source, screenshotBundle, boundsInScreen, insets, taskId, userId, topComponent);
        takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS, completionConsumer);
    }

    private void takeScreenshot(@NonNull Handler handler,
            ScreenshotRequest screenshotRequest, long timeoutMs,
            @Nullable Consumer<Uri> completionConsumer) {
    @VisibleForTesting
    public void takeScreenshotInternal(ScreenshotRequest request, @NonNull Handler handler,
            @Nullable Consumer<Uri> completionConsumer, long timeoutMs) {
        synchronized (mScreenshotLock) {

            final Runnable mScreenshotTimeout = () -> {
@@ -354,7 +122,7 @@ public class ScreenshotHelper {
                }
            };

            Message msg = Message.obtain(null, 0, screenshotRequest);
            Message msg = Message.obtain(null, 0, request);

            Handler h = new Handler(handler.getLooper()) {
                @Override
@@ -471,5 +239,4 @@ public class ScreenshotHelper {
                Intent.FLAG_RECEIVER_FOREGROUND);
        mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
    }

}
+19 −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 com.android.internal.util;

parcelable ScreenshotRequest;
 No newline at end of file
+332 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ android_test {
        "androidx.test.ext.junit",
        "mockito-target-minus-junit4",
        "platform-test-annotations",
        "testng",
    ],

    libs: [
+31 −10
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.internal.util;

import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;

import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;
@@ -31,9 +32,11 @@ import static org.mockito.Mockito.mock;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
import android.hardware.HardwareBuffer;
import android.os.Handler;
import android.os.Looper;
import android.view.WindowManager;
@@ -79,30 +82,48 @@ public final class ScreenshotHelperTest {

    @Test
    public void testFullscreenScreenshot() {
        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
        mScreenshotHelper.takeScreenshot(
                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
    }

    @Test
    public void testFullscreenScreenshotRequest() {
        ScreenshotRequest request = new ScreenshotRequest.Builder(
                TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
                .build();
        mScreenshotHelper.takeScreenshot(request, mHandler, null);
    }

    @Test
    public void testProvidedImageScreenshot() {
        mScreenshotHelper.provideScreenshot(
                new Bundle(), new Rect(), Insets.of(0, 0, 0, 0), 1, 1, new ComponentName("", ""),
                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
        HardwareBuffer buffer = HardwareBuffer.create(
                10, 10, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
        Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
        ScreenshotRequest request = new ScreenshotRequest.Builder(
                TAKE_SCREENSHOT_PROVIDED_IMAGE, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
                .setTopComponent(new ComponentName("", ""))
                .setTaskId(1)
                .setUserId(1)
                .setBitmap(bitmap)
                .setBoundsOnScreen(new Rect())
                .setInsets(Insets.NONE)
                .build();
        mScreenshotHelper.takeScreenshot(request, mHandler, null);
    }

    @Test
    public void testScreenshotTimesOut() {
        long timeoutMs = 10;
        ScreenshotRequest request = new ScreenshotRequest.Builder(
                TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
                .build();

        CountDownLatch lock = new CountDownLatch(1);
        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
                WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
                mHandler,
                timeoutMs,
        mScreenshotHelper.takeScreenshotInternal(request, mHandler,
                uri -> {
                    assertNull(uri);
                    lock.countDown();
                });
                }, timeoutMs);

        try {
            // Add tolerance for delay to prevent flakes.
Loading