Loading core/res/AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -5772,6 +5772,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. --> Loading libs/WindowManager/Shell/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading services/tests/wmtests/AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java 0 → 100644 +234 −0 Original line number Diff line number Diff line /* * 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); } } } Loading
core/res/AndroidManifest.xml +10 −0 Original line number Diff line number Diff line Loading @@ -5772,6 +5772,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. --> Loading
libs/WindowManager/Shell/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading
services/tests/wmtests/AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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 Loading
services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java 0 → 100644 +234 −0 Original line number Diff line number Diff line /* * 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); } } }