/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.hardware.display; import android.app.Presentation; import android.content.Context; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.drawable.ColorDrawable; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.Image; import android.media.ImageReader; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.test.AndroidTestCase; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.widget.ImageView; import java.nio.ByteBuffer; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Tests that applications can create virtual displays and present content on them. * * Contains additional tests that cannot be included in CTS because they require * system permissions. See also the CTS version of VirtualDisplayTest. */ public class VirtualDisplayTest extends AndroidTestCase { private static final String TAG = "VirtualDisplayTest"; private static final String NAME = TAG; private static final int WIDTH = 720; private static final int HEIGHT = 480; private static final int DENSITY = DisplayMetrics.DENSITY_MEDIUM; private static final int TIMEOUT = 10000; // Colors that we use as a signal to determine whether some desired content was // drawn. The colors themselves doesn't matter but we choose them to have with distinct // values for each color channel so as to detect possible RGBA vs. BGRA buffer format issues. // We should only observe RGBA buffers but some graphics drivers might incorrectly // deliver BGRA buffers to virtual displays instead. private static final int BLUEISH = 0xff1122ee; private static final int GREENISH = 0xff33dd44; private DisplayManager mDisplayManager; private Handler mHandler; private final Lock mImageReaderLock = new ReentrantLock(true /*fair*/); private ImageReader mImageReader; private Surface mSurface; private ImageListener mImageListener; @Override protected void setUp() throws Exception { super.setUp(); mDisplayManager = (DisplayManager)mContext.getSystemService(Context.DISPLAY_SERVICE); mHandler = new Handler(Looper.getMainLooper()); mImageListener = new ImageListener(); mImageReaderLock.lock(); try { mImageReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2); mImageReader.setOnImageAvailableListener(mImageListener, mHandler); mSurface = mImageReader.getSurface(); } finally { mImageReaderLock.unlock(); } } @Override protected void tearDown() throws Exception { super.tearDown(); mImageReaderLock.lock(); try { mImageReader.close(); mImageReader = null; mSurface = null; } finally { mImageReaderLock.unlock(); } } /** * Ensures that an application can create a private virtual display and show * its own windows on it. */ public void testPrivateVirtualDisplay() throws Exception { VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME, WIDTH, HEIGHT, DENSITY, mSurface, 0); assertNotNull("virtual display must not be null", virtualDisplay); Display display = virtualDisplay.getDisplay(); try { assertDisplayRegistered(display, Display.FLAG_PRIVATE); // Show a private presentation on the display. assertDisplayCanShowPresentation("private presentation window", display, BLUEISH, WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION, 0); } finally { virtualDisplay.release(); } assertDisplayUnregistered(display); } /** * Ensures that an application can create a private presentation virtual display and show * its own windows on it. */ public void testPrivatePresentationVirtualDisplay() throws Exception { VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME, WIDTH, HEIGHT, DENSITY, mSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); assertNotNull("virtual display must not be null", virtualDisplay); Display display = virtualDisplay.getDisplay(); try { assertDisplayRegistered(display, Display.FLAG_PRIVATE | Display.FLAG_PRESENTATION); // Show a private presentation on the display. assertDisplayCanShowPresentation("private presentation window", display, BLUEISH, WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION, 0); } finally { virtualDisplay.release(); } assertDisplayUnregistered(display); } /** * Ensures that an application can create a public virtual display and show * its own windows on it. This test requires the CAPTURE_VIDEO_OUTPUT permission. * * Because this test does not have an activity token, we use the TOAST window * type to create the window. Another choice might be SYSTEM_ALERT_WINDOW but * that requires a permission. */ public void testPublicPresentationVirtualDisplay() throws Exception { VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME, WIDTH, HEIGHT, DENSITY, mSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); assertNotNull("virtual display must not be null", virtualDisplay); Display display = virtualDisplay.getDisplay(); try { assertDisplayRegistered(display, Display.FLAG_PRESENTATION); // Mirroring case. // Show a window on the default display. It should be mirrored to the // virtual display automatically. Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); assertDisplayCanShowPresentation("mirrored window", defaultDisplay, GREENISH, WindowManager.LayoutParams.TYPE_TOAST, 0); // Mirroring case with secure window (but display is not secure). // Show a window on the default display. It should be replaced with black on // the virtual display. assertDisplayCanShowPresentation("mirrored secure window on non-secure display", defaultDisplay, Color.BLACK, WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_SECURE); // Presentation case. // Show a normal presentation on the display. assertDisplayCanShowPresentation("presentation window", display, BLUEISH, WindowManager.LayoutParams.TYPE_TOAST, 0); // Presentation case with secure window (but display is not secure). // Show a normal presentation on the display. It should be replaced with black. assertDisplayCanShowPresentation("secure presentation window on non-secure display", display, Color.BLACK, WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_SECURE); } finally { virtualDisplay.release(); } assertDisplayUnregistered(display); } /** * Ensures that an application can create a secure public virtual display and show * its own windows on it. This test requires the CAPTURE_SECURE_VIDEO_OUTPUT permission. * * Because this test does not have an activity token, we use the TOAST window * type to create the window. Another choice might be SYSTEM_ALERT_WINDOW but * that requires a permission. */ public void testSecurePublicPresentationVirtualDisplay() throws Exception { VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(NAME, WIDTH, HEIGHT, DENSITY, mSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); assertNotNull("virtual display must not be null", virtualDisplay); Display display = virtualDisplay.getDisplay(); try { assertDisplayRegistered(display, Display.FLAG_PRESENTATION | Display.FLAG_SECURE); // Mirroring case with secure window (and display is secure). // Show a window on the default display. It should be mirrored to the // virtual display automatically. Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); assertDisplayCanShowPresentation("mirrored secure window on secure display", defaultDisplay, GREENISH, WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_SECURE); // Presentation case with secure window (and display is secure). // Show a normal presentation on the display. assertDisplayCanShowPresentation("secure presentation window on secure display", display, BLUEISH, WindowManager.LayoutParams.TYPE_TOAST, WindowManager.LayoutParams.FLAG_SECURE); } finally { virtualDisplay.release(); } assertDisplayUnregistered(display); } private void assertDisplayRegistered(Display display, int flags) { assertNotNull("display object must not be null", display); assertTrue("display must be valid", display.isValid()); assertTrue("display id must be unique", display.getDisplayId() != Display.DEFAULT_DISPLAY); assertEquals("display must have correct flags", flags, display.getFlags()); assertEquals("display name must match supplied name", NAME, display.getName()); Point size = new Point(); display.getSize(size); assertEquals("display width must match supplied width", WIDTH, size.x); assertEquals("display height must match supplied height", HEIGHT, size.y); assertEquals("display rotation must be 0", Surface.ROTATION_0, display.getRotation()); assertNotNull("display must be registered", findDisplay(mDisplayManager.getDisplays(), NAME)); if ((flags & Display.FLAG_PRESENTATION) != 0) { assertNotNull("display must be registered as a presentation display", findDisplay(mDisplayManager.getDisplays( DisplayManager.DISPLAY_CATEGORY_PRESENTATION), NAME)); } else { assertNull("display must not be registered as a presentation display", findDisplay(mDisplayManager.getDisplays( DisplayManager.DISPLAY_CATEGORY_PRESENTATION), NAME)); } } private void assertDisplayUnregistered(Display display) { assertNull("display must no longer be registered after being removed", findDisplay(mDisplayManager.getDisplays(), NAME)); assertFalse("display must no longer be valid", display.isValid()); } private void assertDisplayCanShowPresentation(String message, final Display display, final int color, final int windowType, final int windowFlags) { // At this point, we should not have seen any blue. assertTrue(message + ": display should not show content before window is shown", mImageListener.getColor() != color); final TestPresentation[] presentation = new TestPresentation[1]; try { // Show the presentation. runOnUiThread(new Runnable() { @Override public void run() { presentation[0] = new TestPresentation(getContext(), display, color, windowType, windowFlags); presentation[0].show(); } }); // Wait for the blue to be seen. assertTrue(message + ": display should show content after window is shown", mImageListener.waitForColor(color, TIMEOUT)); } finally { if (presentation[0] != null) { runOnUiThread(new Runnable() { @Override public void run() { presentation[0].dismiss(); } }); } } } private void runOnUiThread(Runnable runnable) { Runnable waiter = new Runnable() { @Override public void run() { synchronized (this) { notifyAll(); } } }; synchronized (waiter) { mHandler.post(runnable); mHandler.post(waiter); try { waiter.wait(TIMEOUT); } catch (InterruptedException ex) { } } } private Display findDisplay(Display[] displays, String name) { for (int i = 0; i < displays.length; i++) { if (displays[i].getName().equals(name)) { return displays[i]; } } return null; } private final class TestPresentation extends Presentation { private final int mColor; private final int mWindowType; private final int mWindowFlags; public TestPresentation(Context context, Display display, int color, int windowType, int windowFlags) { super(context, display); mColor = color; mWindowType = windowType; mWindowFlags = windowFlags; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(TAG); getWindow().setType(mWindowType); getWindow().addFlags(mWindowFlags); // Create a solid color image to use as the content of the presentation. ImageView view = new ImageView(getContext()); view.setImageDrawable(new ColorDrawable(mColor)); view.setLayoutParams(new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setContentView(view); } } /** * Watches for an image with a large amount of some particular solid color to be shown. */ private final class ImageListener implements ImageReader.OnImageAvailableListener { private int mColor = -1; public int getColor() { synchronized (this) { return mColor; } } public boolean waitForColor(int color, long timeoutMillis) { long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis; synchronized (this) { while (mColor != color) { long now = SystemClock.uptimeMillis(); if (now >= timeoutTime) { return false; } try { wait(timeoutTime - now); } catch (InterruptedException ex) { } } return true; } } @Override public void onImageAvailable(ImageReader reader) { mImageReaderLock.lock(); try { if (reader != mImageReader) { return; } Log.d(TAG, "New image available from virtual display."); // Get the latest buffer. Image image = reader.acquireLatestImage(); if (image != null) { try { // Scan for colors. int color = scanImage(image); synchronized (this) { if (mColor != color) { mColor = color; notifyAll(); } } } finally { image.close(); } } } finally { mImageReaderLock.unlock(); } } private int scanImage(Image image) { final Image.Plane plane = image.getPlanes()[0]; final ByteBuffer buffer = plane.getBuffer(); final int width = image.getWidth(); final int height = image.getHeight(); final int pixelStride = plane.getPixelStride(); final int rowStride = plane.getRowStride(); final int rowPadding = rowStride - pixelStride * width; Log.d(TAG, "- Scanning image: width=" + width + ", height=" + height + ", pixelStride=" + pixelStride + ", rowStride=" + rowStride); int offset = 0; int blackPixels = 0; int bluePixels = 0; int greenPixels = 0; int otherPixels = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int pixel = 0; pixel |= (buffer.get(offset) & 0xff) << 16; // R pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G pixel |= (buffer.get(offset + 2) & 0xff); // B pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A if (pixel == Color.BLACK || pixel == 0) { blackPixels += 1; } else if (pixel == BLUEISH) { bluePixels += 1; } else if (pixel == GREENISH) { greenPixels += 1; } else { otherPixels += 1; if (otherPixels < 10) { Log.d(TAG, "- Found unexpected color: " + Integer.toHexString(pixel)); } } offset += pixelStride; } offset += rowPadding; } // Return a color if it represents more than one quarter of the pixels. // We use this threshold in case the display is being letterboxed when // mirroring so there might be large black bars on the sides, which is normal. Log.d(TAG, "- Pixels: " + blackPixels + " black, " + bluePixels + " blue, " + greenPixels + " green, " + otherPixels + " other"); final int threshold = width * height / 4; if (bluePixels > threshold) { Log.d(TAG, "- Reporting blue."); return BLUEISH; } if (greenPixels > threshold) { Log.d(TAG, "- Reporting green."); return GREENISH; } if (blackPixels > threshold) { Log.d(TAG, "- Reporting black."); return Color.BLACK; } Log.d(TAG, "- Reporting other."); return -1; } } }