Merge "Make ScreenshotRequest handle hardware bitmap conversion" into tm-qpr-dev

This commit is contained in:
Miranda Kephart
2023-01-19 17:23:56 +00:00
committed by Android (Google) Code Review
21 changed files with 795 additions and 451 deletions

View File

@@ -1,7 +1,7 @@
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,20 +60,35 @@ 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 handler used to process messages received from the screenshot service
* @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
* 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>
*
* @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 completionConsumer receives the URI of the captured screenshot, once saved or
* null if no screenshot was saved
*/
public void takeScreenshot(ScreenshotRequest request, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
takeScreenshotInternal(request, handler, completionConsumer, SCREENSHOT_TIMEOUT_MS);
}
/**
@@ -299,46 +97,16 @@ public class ScreenshotHelper {
* 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 handler used to process messages received from the screenshot service
* @param timeoutMs time limit for processing, intended only for testing
* @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
* 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);
}
/**
* Request that provided image be handled as if it was a screenshot.
*
* @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 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) {
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);
}
}

View File

@@ -0,0 +1,19 @@
/*
* 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;

View File

@@ -0,0 +1,332 @@
/*
* 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;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.os.UserHandle.USER_NULL;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import android.annotation.NonNull;
import android.content.ComponentName;
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.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.view.WindowManager;
import java.util.Objects;
/**
* Describes a screenshot request.
*/
public class ScreenshotRequest implements Parcelable {
private static final String TAG = "ScreenshotRequest";
@WindowManager.ScreenshotType
private final int mType;
@WindowManager.ScreenshotSource
private final int mSource;
private final ComponentName mTopComponent;
private final int mTaskId;
private final int mUserId;
private final Bitmap mBitmap;
private final Rect mBoundsInScreen;
private final Insets mInsets;
private ScreenshotRequest(
@WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
ComponentName topComponent, int taskId, int userId,
Bitmap bitmap, Rect boundsInScreen, Insets insets) {
mType = type;
mSource = source;
mTopComponent = topComponent;
mTaskId = taskId;
mUserId = userId;
mBitmap = bitmap;
mBoundsInScreen = boundsInScreen;
mInsets = insets;
}
ScreenshotRequest(Parcel in) {
mType = in.readInt();
mSource = in.readInt();
mTopComponent = in.readTypedObject(ComponentName.CREATOR);
mTaskId = in.readInt();
mUserId = in.readInt();
mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
mInsets = in.readTypedObject(Insets.CREATOR);
}
@WindowManager.ScreenshotType
public int getType() {
return mType;
}
@WindowManager.ScreenshotSource
public int getSource() {
return mSource;
}
public Bitmap getBitmap() {
return mBitmap;
}
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);
dest.writeTypedObject(mTopComponent, 0);
dest.writeInt(mTaskId);
dest.writeInt(mUserId);
dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
dest.writeTypedObject(mBoundsInScreen, 0);
dest.writeTypedObject(mInsets, 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];
}
};
/**
* Builder class for {@link ScreenshotRequest} objects.
*/
public static class Builder {
@WindowManager.ScreenshotType
private final int mType;
@WindowManager.ScreenshotSource
private final int mSource;
private Bitmap mBitmap;
private Rect mBoundsInScreen;
private Insets mInsets = Insets.NONE;
private int mTaskId = INVALID_TASK_ID;
private int mUserId = USER_NULL;
private ComponentName mTopComponent;
/**
* Begin building a ScreenshotRequest.
*
* @param type The type of the screenshot request, defined by {@link
* WindowManager.ScreenshotType}
* @param source The source of the screenshot request, defined by {@link
* WindowManager.ScreenshotSource}
*/
public Builder(
@WindowManager.ScreenshotType int type,
@WindowManager.ScreenshotSource int source) {
mType = type;
mSource = source;
}
/**
* Construct a new {@link ScreenshotRequest} with the set parameters.
*/
public ScreenshotRequest build() {
if (mType == TAKE_SCREENSHOT_FULLSCREEN && mBitmap != null) {
Log.w(TAG, "Bitmap provided, but request is fullscreen. Bitmap will be ignored.");
}
if (mType == TAKE_SCREENSHOT_PROVIDED_IMAGE && mBitmap == null) {
throw new IllegalStateException(
"Request is PROVIDED_IMAGE, but no bitmap is provided!");
}
return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
mBoundsInScreen, mInsets);
}
/**
* Set the top component associated with this request.
*
* @param topComponent The component name of the top component running in the task.
*/
public Builder setTopComponent(ComponentName topComponent) {
mTopComponent = topComponent;
return this;
}
/**
* Set the task id associated with this request.
*
* @param taskId The taskId of the task that the screenshot was taken of.
*/
public Builder setTaskId(int taskId) {
mTaskId = taskId;
return this;
}
/**
* Set the user id associated with this request.
*
* @param userId The userId of user running the task provided in taskId.
*/
public Builder setUserId(int userId) {
mUserId = userId;
return this;
}
/**
* Set the bitmap associated with this request.
*
* @param bitmap The provided screenshot.
*/
public Builder setBitmap(Bitmap bitmap) {
mBitmap = bitmap;
return this;
}
/**
* Set the bounds for the provided bitmap.
*
* @param bounds The bounds in screen coordinates that the bitmap originated from.
*/
public Builder setBoundsOnScreen(Rect bounds) {
mBoundsInScreen = bounds;
return this;
}
/**
* Set the insets for the provided bitmap.
*
* @param insets The insets that the image was shown with, inside the screen bounds.
*/
public Builder setInsets(@NonNull Insets insets) {
mInsets = insets;
return this;
}
}
/**
* Bundler used to convert between a hardware bitmap and a bundle without copying the internal
* content. This is used together with a fully-defined ScreenshotRequest to handle a hardware
* bitmap as a screenshot.
*/
private 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)}
*/
private static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
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
*/
private static Bitmap bundleToHardwareBitmap(Bundle bundle) {
if (bundle == null) {
return null;
}
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());
}
}
}

View File

@@ -13,7 +13,7 @@ android_test {
srcs: [
"src/**/*.java",
],
static_libs: [
"frameworks-base-testutils",
"androidx.test.runner",
@@ -21,6 +21,7 @@ android_test {
"androidx.test.ext.junit",
"mockito-target-minus-junit4",
"platform-test-annotations",
"testng",
],
libs: [

View File

@@ -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.

View File

@@ -0,0 +1,139 @@
/*
* 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;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.os.UserHandle.USER_NULL;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.testng.Assert.assertThrows;
import android.content.ComponentName;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Parcel;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class ScreenshotRequestTest {
private final ComponentName mComponentName =
new ComponentName("android.test", "android.test.Component");
@Test
public void testSimpleScreenshot() {
ScreenshotRequest in =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build();
Parcel parcel = Parcel.obtain();
in.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
assertEquals(SCREENSHOT_OTHER, out.getSource());
assertNull("Top component was expected to be null", out.getTopComponent());
assertEquals(INVALID_TASK_ID, out.getTaskId());
assertEquals(USER_NULL, out.getUserId());
assertNull("Bitmap was expected to be null", out.getBitmap());
assertNull("Bounds were expected to be null", out.getBoundsInScreen());
assertEquals(Insets.NONE, out.getInsets());
}
@Test
public void testProvidedScreenshot() {
Bitmap bitmap = makeHardwareBitmap(50, 50);
ScreenshotRequest in =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(mComponentName)
.setTaskId(2)
.setUserId(3)
.setBitmap(bitmap)
.setBoundsOnScreen(new Rect(10, 10, 60, 60))
.setInsets(Insets.of(2, 3, 4, 5))
.build();
Parcel parcel = Parcel.obtain();
in.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
assertEquals(TAKE_SCREENSHOT_PROVIDED_IMAGE, out.getType());
assertEquals(SCREENSHOT_OTHER, out.getSource());
assertEquals(mComponentName, out.getTopComponent());
assertEquals(2, out.getTaskId());
assertEquals(3, out.getUserId());
assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
}
@Test
public void testProvidedScreenshot_nullBitmap() {
ScreenshotRequest.Builder inBuilder =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(mComponentName)
.setTaskId(2)
.setUserId(3)
.setBoundsOnScreen(new Rect(10, 10, 60, 60))
.setInsets(Insets.of(2, 3, 4, 5));
assertThrows(IllegalStateException.class, inBuilder::build);
}
@Test
public void testFullScreenshot_withBitmap() {
// A bitmap added to a FULLSCREEN request will be ignored, but it's technically valid
Bitmap bitmap = makeHardwareBitmap(50, 50);
ScreenshotRequest in =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
.setBitmap(bitmap)
.build();
Parcel parcel = Parcel.obtain();
in.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
assertEquals(SCREENSHOT_OTHER, out.getSource());
assertNull(out.getTopComponent());
assertEquals(INVALID_TASK_ID, out.getTaskId());
assertEquals(USER_NULL, out.getUserId());
assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
assertNull("Bounds expected to be null", out.getBoundsInScreen());
assertEquals(Insets.NONE, out.getInsets());
}
private Bitmap makeHardwareBitmap(int width, int height) {
HardwareBuffer buffer = HardwareBuffer.create(
width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
}
}

View File

@@ -22,6 +22,7 @@ import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.MotionEvent;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.shared.recents.model.Task;
@@ -86,12 +87,6 @@ interface ISystemUiProxy {
*/
void notifyPrioritizedRotation(int rotation) = 25;
/**
* Handle the provided image as if it was a screenshot.
*/
void handleImageBundleAsScreenshot(in Bundle screenImageBundle, in Rect locationInScreen,
in Insets visibleInsets, in Task.TaskKey task) = 28;
/**
* Notifies to expand notification panel.
*/
@@ -125,5 +120,10 @@ interface ISystemUiProxy {
*/
void toggleNotificationPanel() = 50;
// Next id = 51
/**
* Handle the screenshot request.
*/
void takeScreenshot(in ScreenshotRequest request) = 51;
// Next id = 52
}

View File

@@ -42,7 +42,6 @@ import android.view.IWindowManager;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityManager;
@@ -343,6 +342,7 @@ public class SystemActions implements CoreStartable {
/**
* Register a system action.
*
* @param actionId the action ID to register.
*/
public void register(int actionId) {
@@ -440,6 +440,7 @@ public class SystemActions implements CoreStartable {
/**
* Unregister a system action.
*
* @param actionId the action ID to unregister.
*/
public void unregister(int actionId) {
@@ -475,7 +476,8 @@ public class SystemActions implements CoreStartable {
}
private void handleNotifications() {
mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::animateExpandNotificationsPanel);
mCentralSurfacesOptionalLazy.get().ifPresent(
CentralSurfaces::animateExpandNotificationsPanel);
}
private void handleQuickSettings() {
@@ -507,7 +509,7 @@ public class SystemActions implements CoreStartable {
private void handleTakeScreenshot() {
ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
screenshotHelper.takeScreenshot(
SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null);
}

View File

@@ -21,7 +21,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
@@ -959,8 +958,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
mScreenshotHelper.takeScreenshot(SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
}

View File

@@ -21,7 +21,6 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
@@ -44,8 +43,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Binder;
@@ -77,6 +74,7 @@ import com.android.internal.app.IVoiceInteractionSessionListener;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
@@ -94,7 +92,6 @@ import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -322,18 +319,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
@Override
public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
Insets visibleInsets, Task.TaskKey task) {
mScreenshotHelper.provideScreenshot(
screenImageBundle,
locationInScreen,
visibleInsets,
task.id,
task.userId,
task.sourceComponent,
SCREENSHOT_OVERVIEW,
mHandler,
null);
public void takeScreenshot(ScreenshotRequest request) {
mScreenshotHelper.takeScreenshot(request, mHandler, null);
}
@Override

View File

@@ -19,27 +19,26 @@ package com.android.systemui.screenshot
import android.graphics.Insets
import android.util.Log
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.function.Consumer
import javax.inject.Inject
/**
* Processes a screenshot request sent from {@link ScreenshotHelper}.
*/
@SysUISingleton
class RequestProcessor @Inject constructor(
private val capture: ImageCapture,
private val policy: ScreenshotPolicy,
private val flags: FeatureFlags,
/** For the Java Async version, to invoke the callback. */
@Application private val mainScope: CoroutineScope
private val capture: ImageCapture,
private val policy: ScreenshotPolicy,
private val flags: FeatureFlags,
/** For the Java Async version, to invoke the callback. */
@Application private val mainScope: CoroutineScope
) {
/**
* Inspects the incoming request, returning a potentially modified request depending on policy.
@@ -58,7 +57,7 @@ class RequestProcessor @Inject constructor(
// regardless of the managed profile status.
if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
) {
val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
@@ -66,17 +65,21 @@ class RequestProcessor @Inject constructor(
result = if (policy.isManagedProfile(info.user.identifier)) {
val image = capture.captureTask(info.taskId)
?: error("Task snapshot returned a null Bitmap!")
?: error("Task snapshot returned a null Bitmap!")
// Provide the task snapshot as the screenshot
ScreenshotRequest(
TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
HardwareBitmapBundler.hardwareBitmapToBundle(image),
info.bounds, Insets.NONE, info.taskId, info.user.identifier, info.component
)
ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source)
.setTopComponent(info.component)
.setTaskId(info.taskId)
.setUserId(info.user.identifier)
.setBitmap(image)
.setBoundsOnScreen(info.bounds)
.setInsets(Insets.NONE)
.build()
} else {
// Create a new request of the same type which includes the top component
ScreenshotRequest(request.type, request.source, info.component)
ScreenshotRequest.Builder(request.type, request.source)
.setTopComponent(info.component).build()
}
}

View File

@@ -416,10 +416,11 @@ public class ScreenshotController {
}
boolean showFlash = false;
if (!aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
if (screenshotScreenBounds == null
|| !aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
showFlash = true;
visibleInsets = Insets.NONE;
screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight());
screenshotScreenBounds = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight());
}
mCurrentRequestCallback = requestCallback;
saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,

View File

@@ -54,7 +54,7 @@ import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
@@ -186,8 +186,7 @@ public class TakeScreenshotService extends Service {
final Consumer<Uri> onSaved = (uri) -> reportUri(replyTo, uri);
RequestCallback callback = new RequestCallbackImpl(replyTo);
ScreenshotHelper.ScreenshotRequest request =
(ScreenshotHelper.ScreenshotRequest) msg.obj;
ScreenshotRequest request = (ScreenshotRequest) msg.obj;
handleRequest(request, onSaved, callback);
return true;
@@ -195,7 +194,7 @@ public class TakeScreenshotService extends Service {
@MainThread
@VisibleForTesting
void handleRequest(ScreenshotHelper.ScreenshotRequest request, Consumer<Uri> onSaved,
void handleRequest(ScreenshotRequest request, Consumer<Uri> onSaved,
RequestCallback callback) {
// If the storage for this user is locked, we have no place to store
// the screenshot, so skip taking it instead of showing a misleading
@@ -226,7 +225,7 @@ public class TakeScreenshotService extends Service {
(r) -> dispatchToController(r, onSaved, callback));
}
private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
private void dispatchToController(ScreenshotRequest request,
Consumer<Uri> uriConsumer, RequestCallback callback) {
ComponentName topComponent = request.getTopComponent();
@@ -244,8 +243,7 @@ public class TakeScreenshotService extends Service {
if (DEBUG_SERVICE) {
Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE");
}
Bitmap screenshot = ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap(
request.getBitmapBundle());
Bitmap screenshot = request.getBitmap();
Rect screenBounds = request.getBoundsInScreen();
Insets insets = request.getInsets();
int taskId = request.getTaskId();

View File

@@ -22,15 +22,12 @@ import android.graphics.ColorSpace
import android.graphics.Insets
import android.graphics.Rect
import android.hardware.HardwareBuffer
import android.os.Bundle
import android.os.UserHandle
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap
import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
@@ -49,7 +46,6 @@ class RequestProcessorTest {
private val bounds = Rect(25, 25, 75, 75)
private val scope = CoroutineScope(Dispatchers.Unconfined)
private val dispatcher = Dispatchers.Unconfined
private val policy = FakeScreenshotPolicy()
private val flags = FakeFeatureFlags()
@@ -58,7 +54,8 @@ class RequestProcessorTest {
fun testProcessAsync() {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotRequest? = null
@@ -80,7 +77,8 @@ class RequestProcessorTest {
fun testFullScreenshot_workProfilePolicyDisabled() = runBlocking {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -97,9 +95,11 @@ class RequestProcessorTest {
policy.setManagedProfile(USER_ID, false)
policy.setDisplayContentInfo(
policy.getDefaultDisplayId(),
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
)
val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -120,17 +120,20 @@ class RequestProcessorTest {
// Indicate that the primary content belongs to a manged profile
policy.setManagedProfile(USER_ID, true)
policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
policy.setDisplayContentInfo(
policy.getDefaultDisplayId(),
DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
)
val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
// Expect a task snapshot is taken, overriding the full screen mode
assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
assertThat(bitmap.equalsHardwareBitmap(processedRequest.bitmap)).isTrue()
assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
@@ -147,10 +150,16 @@ class RequestProcessorTest {
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val bitmap = makeHardwareBitmap(100, 100)
val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(component)
.setTaskId(TASK_ID)
.setUserId(USER_ID)
.setBitmap(bitmap)
.setBoundsOnScreen(bounds)
.setInsets(Insets.NONE)
.build()
val processedRequest = processor.process(request)
@@ -168,10 +177,16 @@ class RequestProcessorTest {
policy.setManagedProfile(USER_ID, false)
val bitmap = makeHardwareBitmap(100, 100)
val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(component)
.setTaskId(TASK_ID)
.setUserId(USER_ID)
.setBitmap(bitmap)
.setBoundsOnScreen(bounds)
.setInsets(Insets.NONE)
.build()
val processedRequest = processor.process(request)
@@ -190,10 +205,16 @@ class RequestProcessorTest {
policy.setManagedProfile(USER_ID, true)
val bitmap = makeHardwareBitmap(100, 100)
val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(component)
.setTaskId(TASK_ID)
.setUserId(USER_ID)
.setBitmap(bitmap)
.setBoundsOnScreen(bounds)
.setInsets(Insets.NONE)
.build()
val processedRequest = processor.process(request)
@@ -202,14 +223,18 @@ class RequestProcessorTest {
}
private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
val buffer =
HardwareBuffer.create(
width,
height,
HardwareBuffer.RGBA_8888,
1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
)
return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
}
private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean {
val provided = bundleToHardwareBitmap(bundle)
return provided.hardwareBuffer == this.hardwareBuffer &&
provided.colorSpace == this.colorSpace
private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean {
return bitmap.hardwareBuffer == this.hardwareBuffer && bitmap.colorSpace == this.colorSpace
}
}

View File

@@ -35,8 +35,7 @@ import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.ScreenshotHelper
import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
@@ -80,24 +79,39 @@ class TakeScreenshotServiceTest : SysuiTestCase() {
private val flags = FakeFeatureFlags()
private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java)
private val service = TakeScreenshotService(
controller, userManager, devicePolicyManager, eventLogger,
notificationsController, mContext, Runnable::run, flags, requestProcessor)
private val service =
TakeScreenshotService(
controller,
userManager,
devicePolicyManager,
eventLogger,
notificationsController,
mContext,
Runnable::run,
flags,
requestProcessor
)
@Before
fun setUp() {
whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
whenever(devicePolicyManager.getScreenCaptureDisabled(
/* admin component (null: any admin) */ isNull(), eq(UserHandle.USER_ALL)))
whenever(
devicePolicyManager.getScreenCaptureDisabled(
/* admin component (null: any admin) */ isNull(),
eq(UserHandle.USER_ALL)
)
)
.thenReturn(false)
whenever(userManager.isUserUnlocked).thenReturn(true)
// Stub request processor as a synchronous no-op for tests with the flag enabled
doAnswer {
val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
consumer.accept(request)
}.`when`(requestProcessor).processAsync(/* request= */ any(), /* callback= */ any())
val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
consumer.accept(request)
}
.`when`(requestProcessor)
.processAsync(/* request= */ any(), /* callback= */ any())
// Flipped in selected test cases
flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false)
@@ -108,7 +122,8 @@ class TakeScreenshotServiceTest : SysuiTestCase() {
/* className = */ null,
/* token = */ null,
application,
/* activityManager = */ null)
/* activityManager = */ null
)
}
@Test
@@ -125,63 +140,89 @@ class TakeScreenshotServiceTest : SysuiTestCase() {
@Test
fun takeScreenshotFullscreen() {
val request = ScreenshotRequest(
TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_KEY_CHORD,
topComponent)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
.setTopComponent(topComponent)
.build()
service.handleRequest(request, { /* onSaved */ }, callback)
service.handleRequest(request, { /* onSaved */}, callback)
verify(controller, times(1)).takeScreenshotFullscreen(
eq(topComponent),
/* onSavedListener = */ any(),
/* requestCallback = */ any())
verify(controller, times(1))
.takeScreenshotFullscreen(
eq(topComponent),
/* onSavedListener = */ any(),
/* requestCallback = */ any()
)
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id)
assertEquals("Expected supplied package name",
topComponent.packageName, eventLogger.get(0).packageName)
assertEquals(
"Expected SCREENSHOT_REQUESTED UiEvent",
logEvent.eventId,
SCREENSHOT_REQUESTED_KEY_CHORD.id
)
assertEquals(
"Expected supplied package name",
topComponent.packageName,
eventLogger.get(0).packageName
)
}
@Test
fun takeScreenshotProvidedImage() {
val bounds = Rect(50, 50, 150, 150)
val bitmap = makeHardwareBitmap(100, 100)
val bitmapBundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW,
bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, topComponent)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW)
.setTopComponent(topComponent)
.setTaskId(TASK_ID)
.setUserId(USER_ID)
.setBitmap(bitmap)
.setBoundsOnScreen(bounds)
.setInsets(Insets.NONE)
.build()
service.handleRequest(request, { /* onSaved */ }, callback)
service.handleRequest(request, { /* onSaved */}, callback)
verify(controller, times(1)).handleImageAsScreenshot(
argThat { b -> b.equalsHardwareBitmap(bitmap) },
eq(bounds),
eq(Insets.NONE), eq(TASK_ID), eq(USER_ID), eq(topComponent),
/* onSavedListener = */ any(), /* requestCallback = */ any())
verify(controller, times(1))
.handleImageAsScreenshot(
argThat { b -> b.equalsHardwareBitmap(bitmap) },
eq(bounds),
eq(Insets.NONE),
eq(TASK_ID),
eq(USER_ID),
eq(topComponent),
/* onSavedListener = */ any(),
/* requestCallback = */ any()
)
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
assertEquals("Expected SCREENSHOT_REQUESTED_* UiEvent",
logEvent.eventId, SCREENSHOT_REQUESTED_OVERVIEW.id)
assertEquals("Expected supplied package name",
topComponent.packageName, eventLogger.get(0).packageName)
assertEquals(
"Expected SCREENSHOT_REQUESTED_* UiEvent",
logEvent.eventId,
SCREENSHOT_REQUESTED_OVERVIEW.id
)
assertEquals(
"Expected supplied package name",
topComponent.packageName,
eventLogger.get(0).packageName
)
}
@Test
fun takeScreenshotFullscreen_userLocked() {
whenever(userManager.isUserUnlocked).thenReturn(false)
val request = ScreenshotRequest(
TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_KEY_CHORD,
topComponent)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
.setTopComponent(topComponent)
.build()
service.handleRequest(request, { /* onSaved */ }, callback)
service.handleRequest(request, { /* onSaved */}, callback)
verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
verify(callback, times(1)).reportError()
@@ -190,21 +231,24 @@ class TakeScreenshotServiceTest : SysuiTestCase() {
@Test
fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
whenever(devicePolicyManager.getScreenCaptureDisabled(
isNull(), eq(UserHandle.USER_ALL))
).thenReturn(true)
whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
.thenReturn(true)
whenever(devicePolicyResourcesManager.getString(
eq(SCREENSHOT_BLOCKED_BY_ADMIN),
/* Supplier<String> */ any(),
)).thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
whenever(
devicePolicyResourcesManager.getString(
eq(SCREENSHOT_BLOCKED_BY_ADMIN),
/* Supplier<String> */
any(),
)
)
.thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
val request = ScreenshotRequest(
TAKE_SCREENSHOT_FULLSCREEN,
SCREENSHOT_KEY_CHORD,
topComponent)
val request =
ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
.setTopComponent(topComponent)
.build()
service.handleRequest(request, { /* onSaved */ }, callback)
service.handleRequest(request, { /* onSaved */}, callback)
// error shown: Toast.makeText(...).show(), untestable
verify(callback, times(1)).reportError()
@@ -214,14 +258,20 @@ class TakeScreenshotServiceTest : SysuiTestCase() {
private fun Bitmap.equalsHardwareBitmap(other: Bitmap): Boolean {
return config == HARDWARE &&
other.config == HARDWARE &&
hardwareBuffer == other.hardwareBuffer &&
colorSpace == other.colorSpace
other.config == HARDWARE &&
hardwareBuffer == other.hardwareBuffer &&
colorSpace == other.colorSpace
}
/** A hardware Bitmap is mandated by use of ScreenshotHelper.HardwareBitmapBundler */
private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
val buffer =
HardwareBuffer.create(
width,
height,
HardwareBuffer.RGBA_8888,
1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
)
return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
}

View File

@@ -391,7 +391,7 @@ public class SystemActionPerformer {
private boolean takeScreenshot() {
ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
screenshotHelper.takeScreenshot(
WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
new Handler(Looper.getMainLooper()), null);
return true;

View File

@@ -16,6 +16,9 @@
package com.android.server.app;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,7 +36,6 @@ import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.games.CreateGameSessionRequest;
@@ -50,7 +52,6 @@ import android.text.TextUtils;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.WindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -59,6 +60,7 @@ import com.android.internal.infra.ServiceConnector;
import com.android.internal.infra.ServiceConnector.ServiceLifecycleCallbacks;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
@@ -861,8 +863,6 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
Slog.w(TAG, "Could not get bitmap for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
} else {
final Bundle bundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(
bitmap);
final RunningTaskInfo runningTaskInfo =
mGameTaskInfoProvider.getRunningTaskInfo(taskId);
if (runningTaskInfo == null) {
@@ -877,11 +877,17 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan
callback.complete(GameScreenshotResult.createSuccessResult());
}
};
mScreenshotHelper.provideScreenshot(bundle, crop, Insets.NONE, taskId,
mUserHandle.getIdentifier(), gameSessionRecord.getComponentName(),
WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
BackgroundThread.getHandler(),
completionConsumer);
ScreenshotRequest request = new ScreenshotRequest.Builder(
TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
.setTopComponent(gameSessionRecord.getComponentName())
.setTaskId(taskId)
.setUserId(mUserHandle.getIdentifier())
.setBitmap(bitmap)
.setBoundsOnScreen(crop)
.setInsets(Insets.NONE)
.build();
mScreenshotHelper.takeScreenshot(
request, BackgroundThread.getHandler(), completionConsumer);
}
});
}

View File

@@ -709,7 +709,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
handleRingerChordGesture();
break;
case MSG_SCREENSHOT_CHORD:
handleScreenShot(msg.arg1, msg.arg2);
handleScreenShot(msg.arg1);
break;
}
}
@@ -1500,9 +1500,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
|| mShortPressOnStemPrimaryBehavior != SHORT_PRESS_PRIMARY_NOTHING;
}
private void interceptScreenshotChord(int type, int source, long pressDelay) {
private void interceptScreenshotChord(int source, long pressDelay) {
mHandler.removeMessages(MSG_SCREENSHOT_CHORD);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, type, source),
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, source),
pressDelay);
}
@@ -1572,9 +1572,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
};
private void handleScreenShot(@WindowManager.ScreenshotType int type,
@WindowManager.ScreenshotSource int source) {
mDefaultDisplayPolicy.takeScreenshot(type, source);
private void handleScreenShot(@WindowManager.ScreenshotSource int source) {
mDefaultDisplayPolicy.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, source);
}
@Override
@@ -2170,7 +2169,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
@Override
void execute() {
mPowerKeyHandled = true;
interceptScreenshotChord(TAKE_SCREENSHOT_FULLSCREEN,
interceptScreenshotChord(
SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
}
@Override
@@ -2864,8 +2863,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
break;
case KeyEvent.KEYCODE_S:
if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
interceptScreenshotChord(
TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
return key_consumed;
}
break;
@@ -3239,8 +3237,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
break;
case KeyEvent.KEYCODE_SYSRQ:
if (down && repeatCount == 0) {
interceptScreenshotChord(
TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
}
return true;
}

View File

@@ -142,6 +142,7 @@ import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.internal.util.function.TriConsumer;
import com.android.internal.view.AppearanceRegion;
import com.android.internal.widget.PointerLocationView;
@@ -2666,8 +2667,9 @@ public class DisplayPolicy {
*/
public void takeScreenshot(int screenshotType, int source) {
if (mScreenshotHelper != null) {
mScreenshotHelper.takeScreenshot(screenshotType,
source, mHandler, null /* completionConsumer */);
ScreenshotRequest request =
new ScreenshotRequest.Builder(screenshotType, source).build();
mScreenshotHelper.takeScreenshot(request, mHandler, null /* completionConsumer */);
}
}

View File

@@ -1089,8 +1089,7 @@ public final class GameServiceProviderInstanceImplTest {
Consumer<Uri> consumer = invocation.getArgument(invocation.getArguments().length - 1);
consumer.accept(Uri.parse("a/b.png"));
return null;
}).when(mMockScreenshotHelper).provideScreenshot(
any(), any(), any(), anyInt(), anyInt(), any(), anyInt(), any(), any());
}).when(mMockScreenshotHelper).takeScreenshot(any(), any(), any());
mGameServiceProviderInstance.start();
startTask(taskId, GAME_A_MAIN_ACTIVITY);
mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY);

View File

@@ -17,7 +17,6 @@
package com.android.server.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
@@ -301,9 +300,7 @@ public class SystemActionPerformerTest {
mSystemActionPerformer.performSystemAction(
AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
verify(mMockScreenshotHelper).takeScreenshot(
eq(TAKE_SCREENSHOT_FULLSCREEN),
eq(SCREENSHOT_ACCESSIBILITY_ACTIONS),
any(Handler.class), any());
eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
}
// PendingIntent is a final class and cannot be mocked. So we are using this