Add CAPTURE_BLACKOUT_CONTENT permission

Added a permssion to allow an application to capture screenshots of
layers that would normally be blacked out.

Test: Builds, screenshots still work as before
Test: ScreenshotTests
Bug: 173746627
Change-Id: I27237c8e340904cbaa82c7e5793de35cab85d8de
This commit is contained in:
chaviw
2021-04-28 15:33:21 -05:00
parent e13111babd
commit fb796315ed
4 changed files with 247 additions and 0 deletions

View File

@@ -5768,6 +5768,16 @@
android:protectionLevel="normal" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/>
<!-- Allows an application to take screenshots of layers that normally would be blacked out when
a screenshot is taken. Specifically, layers that have the flag
{@link android.view.SurfaceControl#SECURE} will be screenshot if the caller requests to
capture secure layers. Normally those layers will be rendered black.
<p>Not for use by third-party applications.
@hide
-->
<permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT"
android:protectionLevel="signature" />
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->

View File

@@ -18,6 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.wm.shell">
<!-- System permission required by WM Shell Task Organizer. -->
<uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />

View File

@@ -41,6 +41,7 @@
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
<uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT"/>
<!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
<application android:debuggable="true"
@@ -75,6 +76,7 @@
<activity android:name="com.android.server.wm.ActivityOptionsTest$MainActivity"
android:turnScreenOn="true"
android:showWhenLocked="true" />
<activity android:name="com.android.server.wm.ScreenshotTests$ScreenshotActivity" />
</application>
<instrumentation

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2021 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.server.wm;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorSpace;
import android.graphics.GraphicBuffer;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Build/Install/Run:
* atest WmTests:ScreenshotTests
*/
@SmallTest
@Presubmit
public class ScreenshotTests {
private static final int BUFFER_WIDTH = 100;
private static final int BUFFER_HEIGHT = 100;
private final Instrumentation mInstrumentation = getInstrumentation();
@Rule
public ActivityTestRule<ScreenshotActivity> mActivityRule =
new ActivityTestRule<>(ScreenshotActivity.class);
private ScreenshotActivity mActivity;
@Before
public void setup() {
mActivity = mActivityRule.getActivity();
mInstrumentation.waitForIdleSync();
}
@Test
public void testScreenshotSecureLayers() {
SurfaceControl secureSC = new SurfaceControl.Builder()
.setName("SecureChildSurfaceControl")
.setBLASTLayer()
.setCallsite("makeSecureSurfaceControl")
.setSecure(true)
.build();
SurfaceControl.Transaction t = mActivity.addChildSc(secureSC);
mInstrumentation.waitForIdleSync();
GraphicBuffer buffer = GraphicBuffer.create(BUFFER_WIDTH, BUFFER_HEIGHT,
PixelFormat.RGBA_8888,
GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER
| GraphicBuffer.USAGE_SW_WRITE_RARELY);
Canvas canvas = buffer.lockCanvas();
canvas.drawColor(Color.RED);
buffer.unlockCanvasAndPost(canvas);
t.show(secureSC)
.setBuffer(secureSC, buffer)
.setColorSpace(secureSC, ColorSpace.get(ColorSpace.Named.SRGB))
.apply(true);
SurfaceControl.LayerCaptureArgs args = new SurfaceControl.LayerCaptureArgs.Builder(secureSC)
.setCaptureSecureLayers(true)
.setChildrenOnly(false)
.build();
SurfaceControl.ScreenshotHardwareBuffer hardwareBuffer = SurfaceControl.captureLayers(args);
assertNotNull(hardwareBuffer);
Bitmap screenshot = hardwareBuffer.asBitmap();
assertNotNull(screenshot);
Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false);
screenshot.recycle();
int numMatchingPixels = PixelChecker.getNumMatchingPixels(swBitmap,
new PixelColor(PixelColor.RED));
long sizeOfBitmap = swBitmap.getWidth() * swBitmap.getHeight();
boolean success = numMatchingPixels == sizeOfBitmap;
swBitmap.recycle();
assertTrue(success);
}
public static class ScreenshotActivity extends Activity {
private static final long WAIT_TIMEOUT_S = 5;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().getDecorView().setPointerIcon(
PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
SurfaceControl.Transaction addChildSc(SurfaceControl surfaceControl) {
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
CountDownLatch countDownLatch = new CountDownLatch(1);
mHandler.post(() -> {
t.merge(getWindow().getRootSurfaceControl().buildReparentTransaction(
surfaceControl));
countDownLatch.countDown();
});
try {
countDownLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
return t;
}
}
public abstract static class PixelChecker {
static int getNumMatchingPixels(Bitmap bitmap, PixelColor pixelColor) {
int numMatchingPixels = 0;
for (int x = 0; x < bitmap.getWidth(); x++) {
for (int y = 0; y < bitmap.getHeight(); y++) {
int color = bitmap.getPixel(x, y);
if (matchesColor(pixelColor, color)) {
numMatchingPixels++;
}
}
}
return numMatchingPixels;
}
static boolean matchesColor(PixelColor expectedColor, int color) {
final float red = Color.red(color);
final float green = Color.green(color);
final float blue = Color.blue(color);
final float alpha = Color.alpha(color);
return alpha <= expectedColor.mMaxAlpha
&& alpha >= expectedColor.mMinAlpha
&& red <= expectedColor.mMaxRed
&& red >= expectedColor.mMinRed
&& green <= expectedColor.mMaxGreen
&& green >= expectedColor.mMinGreen
&& blue <= expectedColor.mMaxBlue
&& blue >= expectedColor.mMinBlue;
}
}
public static class PixelColor {
public static final int BLACK = 0xFF000000;
public static final int RED = 0xFF0000FF;
public static final int GREEN = 0xFF00FF00;
public static final int BLUE = 0xFFFF0000;
public static final int YELLOW = 0xFF00FFFF;
public static final int MAGENTA = 0xFFFF00FF;
public static final int WHITE = 0xFFFFFFFF;
public static final int TRANSPARENT_RED = 0x7F0000FF;
public static final int TRANSPARENT_BLUE = 0x7FFF0000;
public static final int TRANSPARENT = 0x00000000;
// Default to black
public short mMinAlpha;
public short mMaxAlpha;
public short mMinRed;
public short mMaxRed;
public short mMinBlue;
public short mMaxBlue;
public short mMinGreen;
public short mMaxGreen;
public PixelColor(int color) {
short alpha = (short) ((color >> 24) & 0xFF);
short blue = (short) ((color >> 16) & 0xFF);
short green = (short) ((color >> 8) & 0xFF);
short red = (short) (color & 0xFF);
mMinAlpha = (short) getMinValue(alpha);
mMaxAlpha = (short) getMaxValue(alpha);
mMinRed = (short) getMinValue(red);
mMaxRed = (short) getMaxValue(red);
mMinBlue = (short) getMinValue(blue);
mMaxBlue = (short) getMaxValue(blue);
mMinGreen = (short) getMinValue(green);
mMaxGreen = (short) getMaxValue(green);
}
public PixelColor() {
this(BLACK);
}
private int getMinValue(short color) {
return Math.max(color - 4, 0);
}
private int getMaxValue(short color) {
return Math.min(color + 4, 0xFF);
}
}
}