/*
* 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;
}
}
}