Merge "camera2 CTS: Allow AE convergence in DngCreatorTest." into mnc-dev
diff --git a/libs/deviceutillegacy/src/android/webkit/cts/WebViewOnUiThread.java b/libs/deviceutillegacy/src/android/webkit/cts/WebViewOnUiThread.java
index 5cd6f30..6f310dd 100644
--- a/libs/deviceutillegacy/src/android/webkit/cts/WebViewOnUiThread.java
+++ b/libs/deviceutillegacy/src/android/webkit/cts/WebViewOnUiThread.java
@@ -21,6 +21,7 @@
 import android.graphics.Bitmap;
 import android.graphics.Picture;
 import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Looper;
 import android.os.Message;
@@ -36,6 +37,8 @@
 import android.webkit.ValueCallback;
 import android.webkit.WebBackForwardList;
 import android.webkit.WebChromeClient;
+import android.webkit.WebMessage;
+import android.webkit.WebMessagePort;
 import android.webkit.WebSettings;
 import android.webkit.WebView.HitTestResult;
 import android.webkit.WebView.PictureListener;
@@ -307,6 +310,24 @@
         });
     }
 
+    public WebMessagePort[] createWebMessageChannel() {
+        return getValue(new ValueGetter<WebMessagePort[]>() {
+            @Override
+            public WebMessagePort[] capture() {
+                return mWebView.createWebMessageChannel();
+            }
+        });
+    }
+
+    public void postMessageToMainFrame(final WebMessage message, final Uri targetOrigin) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.postMessageToMainFrame(message, targetOrigin);
+            }
+        });
+    }
+
     public void addJavascriptInterface(final Object object, final String name) {
         runOnUiThread(new Runnable() {
             @Override
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java b/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
index ea333eb..08f628f 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
@@ -61,10 +61,12 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A package private utility class for wrapping up the camera2 cts test common utility functions
@@ -284,6 +286,35 @@
         }
     }
 
+    public static class SimpleImageWriterListener implements ImageWriter.ImageListener {
+        private final Semaphore mImageReleasedSema = new Semaphore(0);
+        private final ImageWriter mWriter;
+        @Override
+        public void onInputImageReleased(ImageWriter writer) {
+            if (writer != mWriter) {
+                return;
+            }
+
+            if (VERBOSE) {
+                Log.v(TAG, "Input image is released");
+            }
+            mImageReleasedSema.release();
+        }
+
+        public SimpleImageWriterListener(ImageWriter writer) {
+            if (writer == null) {
+                throw new IllegalArgumentException("writer cannot be null");
+            }
+            mWriter = writer;
+        }
+
+        public void waitForImageReleased(long timeoutMs) throws InterruptedException {
+            if (!mImageReleasedSema.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
+                fail("wait for image available timed out after " + timeoutMs + "ms");
+            }
+        }
+    }
+
     public static class SimpleCaptureCallback extends CameraCaptureSession.CaptureCallback {
         private final LinkedBlockingQueue<TotalCaptureResult> mQueue =
                 new LinkedBlockingQueue<TotalCaptureResult>();
@@ -391,16 +422,63 @@
          */
         public TotalCaptureResult getTotalCaptureResultForRequest(CaptureRequest myRequest,
                 int numResultsWait) {
+            ArrayList<CaptureRequest> captureRequests = new ArrayList<>(1);
+            captureRequests.add(myRequest);
+            return getTotalCaptureResultsForRequests(captureRequests, numResultsWait)[0];
+        }
+
+        /**
+         * Get an array of {@link #TotalCaptureResult total capture results} for a given list of
+         * {@link #CaptureRequest capture requests}. This can be used when the order of results
+         * may not the same as the order of requests.
+         *
+         * @param captureRequests The list of {@link #CaptureRequest capture requests} whose
+         *            corresponding {@link #TotalCaptureResult capture results} are
+         *            being waited for.
+         * @param numResultsWait Number of frames to wait for the capture results
+         *            before timeout.
+         * @throws TimeoutRuntimeException If more than numResultsWait results are
+         *            seen before all the results matching captureRequests arrives.
+         */
+        public TotalCaptureResult[] getTotalCaptureResultsForRequests(
+                List<CaptureRequest> captureRequests, int numResultsWait) {
             if (numResultsWait < 0) {
                 throw new IllegalArgumentException("numResultsWait must be no less than 0");
             }
+            if (captureRequests == null || captureRequests.size() == 0) {
+                throw new IllegalArgumentException("captureRequests must have at least 1 request.");
+            }
 
-            TotalCaptureResult result;
+            // Create a request -> a list of result indices map that it will wait for.
+            HashMap<CaptureRequest, ArrayList<Integer>> remainingResultIndicesMap = new HashMap<>();
+            for (int i = 0; i < captureRequests.size(); i++) {
+                CaptureRequest request = captureRequests.get(i);
+                ArrayList<Integer> indices = remainingResultIndicesMap.get(request);
+                if (indices == null) {
+                    indices = new ArrayList<>();
+                    remainingResultIndicesMap.put(request, indices);
+                }
+                indices.add(i);
+            }
+
+            TotalCaptureResult[] results = new TotalCaptureResult[captureRequests.size()];
             int i = 0;
             do {
-                result = getTotalCaptureResult(CAPTURE_RESULT_TIMEOUT_MS);
-                if (result.getRequest().equals(myRequest)) {
-                    return result;
+                TotalCaptureResult result = getTotalCaptureResult(CAPTURE_RESULT_TIMEOUT_MS);
+                CaptureRequest request = result.getRequest();
+                ArrayList<Integer> indices = remainingResultIndicesMap.get(request);
+                if (indices != null) {
+                    results[indices.get(0)] = result;
+                    indices.remove(0);
+
+                    // Remove the entry if all results for this request has been fulfilled.
+                    if (indices.isEmpty()) {
+                        remainingResultIndicesMap.remove(request);
+                    }
+                }
+
+                if (remainingResultIndicesMap.isEmpty()) {
+                    return results;
                 }
             } while (i++ < numResultsWait);
 
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/ImageWriterTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/ImageWriterTest.java
index 1ce32a4..d9e5cdf 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/ImageWriterTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/ImageWriterTest.java
@@ -132,27 +132,6 @@
         }
     }
 
-    private final class SimpleImageWriterListener implements ImageWriter.ImageListener {
-        private final ConditionVariable imageReleased = new ConditionVariable();
-        @Override
-        public void onInputImageReleased(ImageWriter writer) {
-            if (writer != mWriter) {
-                return;
-            }
-
-            if (VERBOSE) Log.v(TAG, "Input image is released");
-            imageReleased.open();
-        }
-
-        public void waitForImageReleassed(long timeoutMs) {
-            if (imageReleased.block(timeoutMs)) {
-                imageReleased.close();
-            } else {
-                fail("wait for image available timed out after " + timeoutMs + "ms");
-            }
-        }
-    }
-
     private void readerWriterFormatTestByCamera(int format)  throws Exception {
         List<Size> sizes = getSortedSizesForFormat(mCamera.getId(), mCameraManager, format, null);
         Size maxSize = sizes.get(0);
@@ -178,7 +157,7 @@
         Surface surface = mReaderForWriter.getSurface();
         assertNotNull("Surface from ImageReader shouldn't be null", surface);
         mWriter = ImageWriter.newInstance(surface, MAX_NUM_IMAGES);
-        SimpleImageWriterListener writerImageListener = new SimpleImageWriterListener();
+        SimpleImageWriterListener writerImageListener = new SimpleImageWriterListener(mWriter);
         mWriter.setImageListener(writerImageListener, mHandler);
 
         // Start capture: capture 2 images.
@@ -257,7 +236,7 @@
             outputImage.close();
 
             // Make sure ImageWriter listener callback is fired.
-            writerImageListener.waitForImageReleassed(CAPTURE_IMAGE_TIMEOUT_MS);
+            writerImageListener.waitForImageReleased(CAPTURE_IMAGE_TIMEOUT_MS);
 
             // Test case 2: Directly inject the image into ImageWriter: works for all formats.
 
@@ -291,7 +270,7 @@
             outputImage.close();
 
             // Make sure ImageWriter listener callback is fired.
-            writerImageListener.waitForImageReleassed(CAPTURE_IMAGE_TIMEOUT_MS);
+            writerImageListener.waitForImageReleased(CAPTURE_IMAGE_TIMEOUT_MS);
         }
 
         stopCapture(/*fast*/false);
@@ -321,24 +300,16 @@
             validateOpaqueImage(outputImage, "First Opaque image output by ImageWriter: ",
                     maxSize, result);
             outputImage.close();
-            writerListener.waitForImageReleassed(CAPTURE_IMAGE_TIMEOUT_MS);
+            writerListener.waitForImageReleased(CAPTURE_IMAGE_TIMEOUT_MS);
         }
     }
 
     private void validateOpaqueImage(Image image, String msg, Size imageSize,
             CaptureResult result) {
         assertNotNull("Opaque image Capture result should not be null", result != null);
-        mCollector.expectTrue(msg + "Opaque image format should be: " + CAMERA_OPAQUE_FORMAT,
-                image.getFormat() == CAMERA_OPAQUE_FORMAT);
-        mCollector.expectTrue(msg + "Opaque image format should be: " + CAMERA_OPAQUE_FORMAT,
-                image.getFormat() == CAMERA_OPAQUE_FORMAT);
+        mCollector.expectImageProperties(msg + "Opaque ", image, CAMERA_OPAQUE_FORMAT,
+                imageSize, result.get(CaptureResult.SENSOR_TIMESTAMP));
         mCollector.expectTrue(msg + "Opaque image number planes should be zero",
                 image.getPlanes().length == 0);
-        mCollector.expectTrue(msg + "Opaque image size should be " + imageSize,
-                image.getWidth() == imageSize.getWidth() &&
-                image.getHeight() == imageSize.getHeight());
-        long timestampNs = result.get(CaptureResult.SENSOR_TIMESTAMP);
-        mCollector.expectTrue(msg + "Opaque image timestamp should be " + timestampNs,
-                image.getTimestamp() == timestampNs);
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
index dfd1aa8..7d6ab55 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
@@ -25,6 +25,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
 import android.hardware.camera2.cts.helpers.StaticMetadata;
 import android.hardware.camera2.cts.helpers.StaticMetadata.CheckLevel;
@@ -48,8 +49,6 @@
     private static final String TAG = "ReprocessCaptureTest";
     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final int MAX_NUM_IMAGE_READER_IMAGES = 3;
-    private static final int MAX_NUM_IMAGE_WRITER_IMAGES = 3;
     private static final int CAPTURE_TIMEOUT_FRAMES = 100;
     private static final int CAPTURE_TIMEOUT_MS = 3000;
     private static final int WAIT_FOR_SURFACE_CHANGE_TIMEOUT_MS = 1000;
@@ -57,16 +56,27 @@
     private static final int PREVIEW_TEMPLATE = CameraDevice.TEMPLATE_PREVIEW;
     private static final int NUM_REPROCESS_TEST_LOOP = 3;
     private static final int NUM_REPROCESS_CAPTURES = 3;
+    private static final int NUM_REPROCESS_BURST = 3;
     private int mDumpFrameCount = 0;
 
     // The image reader for the first regular capture
     private ImageReader mFirstImageReader;
     // The image reader for the reprocess capture
     private ImageReader mSecondImageReader;
+    // A flag indicating whether the regular capture and the reprocess capture share the same image
+    // reader. If it's true, mFirstImageReader should be used for regular and reprocess outputs.
+    private boolean mShareOneImageReader;
     private SimpleImageReaderListener mFirstImageReaderListener;
     private SimpleImageReaderListener mSecondImageReaderListener;
     private Surface mInputSurface;
     private ImageWriter mImageWriter;
+    private SimpleImageWriterListener mImageWriterListener;
+
+    private enum CaptureTestCase {
+        SINGLE_SHOT,
+        BURST,
+        MIXED_BURST
+    }
 
     /**
      * Test YUV_420_888 -> YUV_420_888 with maximal supported sizes
@@ -137,7 +147,8 @@
                 // open Camera device
                 openDevice(id);
                 // no preview
-                testReprocessingAllCombinations(id, null);
+                testReprocessingAllCombinations(id, /*previewSize*/null,
+                        CaptureTestCase.SINGLE_SHOT);
             } finally {
                 closeDevice();
             }
@@ -156,7 +167,8 @@
             try {
                 // open Camera device
                 openDevice(id);
-                testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0));
+                testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0),
+                        CaptureTestCase.SINGLE_SHOT);
             } finally {
                 closeDevice();
             }
@@ -234,8 +246,8 @@
                 }
 
                 setupImageReaders(inputSize, inputFormat, reprocessOutputSize,
-                        reprocessOutputFormat);
-                setupReprocessibleSession(/*previewSurface*/null);
+                        reprocessOutputFormat, /*maxImages*/1);
+                setupReprocessibleSession(/*previewSurface*/null, /*numImageWriterImages*/1);
 
                 TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
                         /*inputResult*/null);
@@ -246,11 +258,17 @@
 
                 // recreate the session
                 closeReprossibleSession();
-                setupReprocessibleSession(/*previewSurface*/null);
+                setupReprocessibleSession(/*previewSurface*/null, /*numImageWriterImages*/1);
                 try {
+                    TotalCaptureResult reprocessResult;
                     // issue and wait on reprocess capture request
-                    TotalCaptureResult reprocessResult =
-                            submitCaptureRequest(mSecondImageReader.getSurface(), result);
+                    if (mShareOneImageReader) {
+                        reprocessResult =
+                                submitCaptureRequest(mFirstImageReader.getSurface(), result);
+                    } else {
+                        reprocessResult =
+                                submitCaptureRequest(mSecondImageReader.getSurface(), result);
+                    }
                     fail("Camera " + id + ": should get IllegalArgumentException for cross " +
                             "session reprocess captrue.");
                 } catch (IllegalArgumentException e) {
@@ -268,8 +286,52 @@
         }
     }
 
-    // todo: test aborting reprocessing captures.
-    // todo: test burst reprocessing captures.
+    /**
+     * Test burst reprocessing captures with and without preview.
+     */
+    public void testBurstReprocessing() throws Exception {
+        for (String id : mCameraIds) {
+            if (!isYuvReprocessSupported(id) && !isOpaqueReprocessSupported(id)) {
+                continue;
+            }
+
+            try {
+                // open Camera device
+                openDevice(id);
+                // no preview
+                testReprocessingAllCombinations(id, /*previewSize*/null, CaptureTestCase.BURST);
+                // with preview
+                testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0),
+                        CaptureTestCase.BURST);
+            } finally {
+                closeDevice();
+            }
+        }
+    }
+
+    /**
+     * Test burst captures mixed with regular and reprocess captures with and without preview.
+     */
+    public void testMixedBurstReprocessing() throws Exception {
+        for (String id : mCameraIds) {
+            if (!isYuvReprocessSupported(id) && !isOpaqueReprocessSupported(id)) {
+                continue;
+            }
+
+            try {
+                // open Camera device
+                openDevice(id);
+                // no preview
+                testReprocessingAllCombinations(id, /*previewSize*/null,
+                        CaptureTestCase.MIXED_BURST);
+                // with preview
+                testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0),
+                        CaptureTestCase.MIXED_BURST);
+            } finally {
+                closeDevice();
+            }
+        }
+    }
 
     /**
      * Test the input format and output format with the largest input and output sizes.
@@ -294,8 +356,8 @@
     /**
      * Test all input format, input size, output format, and output size combinations.
      */
-    private void testReprocessingAllCombinations(String cameraId,
-            Size previewSize) throws Exception {
+    private void testReprocessingAllCombinations(String cameraId, Size previewSize,
+            CaptureTestCase captureTestCase) throws Exception {
 
         int[] supportedInputFormats =
                 mStaticInfo.getAvailableFormats(StaticMetadata.StreamDirection.Input);
@@ -314,15 +376,165 @@
                             StaticMetadata.StreamDirection.Output);
 
                     for (Size reprocessOutputSize : supportedReprocessOutputSizes) {
-                        testReprocess(cameraId, inputSize, inputFormat,
-                                reprocessOutputSize, reprocessOutputFormat, previewSize,
-                                NUM_REPROCESS_CAPTURES);
+                        switch (captureTestCase) {
+                            case SINGLE_SHOT:
+                                testReprocess(cameraId, inputSize, inputFormat,
+                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
+                                        NUM_REPROCESS_CAPTURES);
+                                break;
+                            case BURST:
+                                testReprocessBurst(cameraId, inputSize, inputFormat,
+                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
+                                        NUM_REPROCESS_BURST);
+                                break;
+                            case MIXED_BURST:
+                                testReprocessMixedBurst(cameraId, inputSize, inputFormat,
+                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
+                                        NUM_REPROCESS_BURST);
+                                break;
+                            default:
+                                throw new IllegalArgumentException("Invalid capture type");
+                        }
                     }
                 }
             }
         }
     }
 
+    /**
+     * Test burst that is mixed with regular and reprocess capture requests.
+     */
+    private void testReprocessMixedBurst(String cameraId, Size inputSize, int inputFormat,
+            Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
+            int numBurst) throws Exception {
+        if (VERBOSE) {
+            Log.v(TAG, "testReprocessMixedBurst: cameraId: " + cameraId + " inputSize: " +
+                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
+                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat +
+                    " previewSize: " + previewSize + " numBurst: " + numBurst);
+        }
+
+        boolean enablePreview = (previewSize != null);
+        ImageResultHolder[] imageResultHolders = new ImageResultHolder[0];
+
+        try {
+            // totalNumBurst = number of regular burst + number of reprocess burst.
+            int totalNumBurst = numBurst * 2;
+
+            if (enablePreview) {
+                updatePreviewSurface(previewSize);
+            } else {
+                mPreviewSurface = null;
+            }
+
+            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
+                totalNumBurst);
+            setupReprocessibleSession(mPreviewSurface, /*numImageWriterImages*/numBurst);
+
+            if (enablePreview) {
+                startPreview(mPreviewSurface);
+            }
+
+            // Prepare an array of booleans indicating each capture's type (regular or reprocess)
+            boolean[] isReprocessCaptures = new boolean[totalNumBurst];
+            for (int i = 0; i < totalNumBurst; i++) {
+                if ((i & 1) == 0) {
+                    isReprocessCaptures[i] = true;
+                } else {
+                    isReprocessCaptures[i] = false;
+                }
+            }
+
+            imageResultHolders = doMixedReprocessBurstCapture(isReprocessCaptures);
+            for (ImageResultHolder holder : imageResultHolders) {
+                Image reprocessedImage = holder.getImage();
+                TotalCaptureResult result = holder.getTotalCaptureResult();
+
+                mCollector.expectImageProperties("testReprocessMixedBurst", reprocessedImage,
+                            reprocessOutputFormat, reprocessOutputSize,
+                            result.get(CaptureResult.SENSOR_TIMESTAMP));
+
+                if (DEBUG) {
+                    Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
+                            cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
+                            reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
+                            reprocessOutputFormat));
+                    dumpImage(reprocessedImage,
+                            "/testReprocessMixedBurst_camera" + cameraId + "_" + mDumpFrameCount);
+                    mDumpFrameCount++;
+                }
+            }
+        } finally {
+            for (ImageResultHolder holder : imageResultHolders) {
+                holder.getImage().close();
+            }
+            closeReprossibleSession();
+            closeImageReaders();
+        }
+    }
+
+    /**
+     * Test burst of reprocess capture requests.
+     */
+    private void testReprocessBurst(String cameraId, Size inputSize, int inputFormat,
+            Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
+            int numBurst) throws Exception {
+        if (VERBOSE) {
+            Log.v(TAG, "testReprocessBurst: cameraId: " + cameraId + " inputSize: " +
+                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
+                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat +
+                    " previewSize: " + previewSize + " numBurst: " + numBurst);
+        }
+
+        boolean enablePreview = (previewSize != null);
+        ImageResultHolder[] imageResultHolders = new ImageResultHolder[0];
+
+        try {
+            if (enablePreview) {
+                updatePreviewSurface(previewSize);
+            } else {
+                mPreviewSurface = null;
+            }
+
+            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
+                numBurst);
+            setupReprocessibleSession(mPreviewSurface, numBurst);
+
+            if (enablePreview) {
+                startPreview(mPreviewSurface);
+            }
+
+            imageResultHolders = doReprocessBurstCapture(numBurst);
+            for (ImageResultHolder holder : imageResultHolders) {
+                Image reprocessedImage = holder.getImage();
+                TotalCaptureResult result = holder.getTotalCaptureResult();
+
+                mCollector.expectImageProperties("testReprocessBurst", reprocessedImage,
+                            reprocessOutputFormat, reprocessOutputSize,
+                            result.get(CaptureResult.SENSOR_TIMESTAMP));
+
+                if (DEBUG) {
+                    Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
+                            cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
+                            reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
+                            reprocessOutputFormat));
+                    dumpImage(reprocessedImage,
+                            "/testReprocessBurst_camera" + cameraId + "_" + mDumpFrameCount);
+                    mDumpFrameCount++;
+                }
+            }
+        } finally {
+            for (ImageResultHolder holder : imageResultHolders) {
+                holder.getImage().close();
+            }
+            closeReprossibleSession();
+            closeImageReaders();
+        }
+    }
+
+    /**
+     * Test a sequences of reprocess capture requests.
+     */
     private void testReprocess(String cameraId, Size inputSize, int inputFormat,
             Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
             int numReprocessCaptures) throws Exception {
@@ -342,57 +554,39 @@
                 mPreviewSurface = null;
             }
 
-            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat);
-            setupReprocessibleSession(mPreviewSurface);
+            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
+                    /*maxImages*/1);
+            setupReprocessibleSession(mPreviewSurface, /*numImageWriterImages*/1);
 
             if (enablePreview) {
                 startPreview(mPreviewSurface);
             }
 
             for (int i = 0; i < numReprocessCaptures; i++) {
-                Image reprocessedImage = null;
+                ImageResultHolder imageResultHolder = null;
 
                 try {
-                    reprocessedImage = doReprocessCapture();
+                    imageResultHolder = doReprocessCapture();
+                    Image reprocessedImage = imageResultHolder.getImage();
+                    TotalCaptureResult result = imageResultHolder.getTotalCaptureResult();
 
-                    assertTrue(String.format("Reprocess output size is %dx%d. Expecting %dx%d.",
-                            reprocessedImage.getWidth(), reprocessedImage.getHeight(),
-                            reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight()),
-                            reprocessedImage.getWidth() == reprocessOutputSize.getWidth() &&
-                            reprocessedImage.getHeight() == reprocessOutputSize.getHeight());
-                    assertTrue(String.format("Reprocess output format is %d. Expecting %d.",
-                            reprocessedImage.getFormat(), reprocessOutputFormat),
-                            reprocessedImage.getFormat() == reprocessOutputFormat);
+                    mCollector.expectImageProperties("testReprocess", reprocessedImage,
+                            reprocessOutputFormat, reprocessOutputSize,
+                            result.get(CaptureResult.SENSOR_TIMESTAMP));
 
                     if (DEBUG) {
-                        String filename = DEBUG_FILE_NAME_BASE + "/reprocessed_camera" + cameraId +
-                                "_" + mDumpFrameCount;
-                        mDumpFrameCount++;
-
-                        switch(reprocessedImage.getFormat()) {
-                            case ImageFormat.JPEG:
-                                filename += ".jpg";
-                                break;
-                            case ImageFormat.NV16:
-                            case ImageFormat.NV21:
-                            case ImageFormat.YUV_420_888:
-                                filename += ".yuv";
-                                break;
-                            default:
-                                filename += "." + reprocessedImage.getFormat();
-                                break;
-                        }
-
-                        Log.d(TAG, "dumping an image to " + filename);
                         Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
                                 cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
                                 reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
                                 reprocessOutputFormat));
-                        dumpFile(filename , getDataFromImage(reprocessedImage));
+
+                        dumpImage(reprocessedImage,
+                                "/testReprocess_camera" + cameraId + "_" + mDumpFrameCount);
+                        mDumpFrameCount++;
                     }
                 } finally {
-                    if (reprocessedImage != null) {
-                        reprocessedImage.close();
+                    if (imageResultHolder != null) {
+                        imageResultHolder.getImage().close();
                     }
                 }
             }
@@ -402,20 +596,37 @@
         }
     }
 
+    /**
+     * Set up two image readers: one for regular capture (used for reprocess input) and one for
+     * reprocess capture.
+     */
     private void setupImageReaders(Size inputSize, int inputFormat, Size reprocessOutputSize,
-            int reprocessOutputFormat) {
+            int reprocessOutputFormat, int maxImages) {
 
+        mShareOneImageReader = false;
+        // If the regular output and reprocess output have the same size and format,
+        // they can share one image reader.
+        if (inputFormat == reprocessOutputFormat &&
+                inputSize.equals(reprocessOutputSize)) {
+            maxImages *= 2;
+            mShareOneImageReader = true;
+        }
         // create an ImageReader for the regular capture
         mFirstImageReaderListener = new SimpleImageReaderListener();
-        mFirstImageReader = makeImageReader(inputSize, inputFormat,
-                MAX_NUM_IMAGE_READER_IMAGES, mFirstImageReaderListener, mHandler);
+        mFirstImageReader = makeImageReader(inputSize, inputFormat, maxImages,
+                mFirstImageReaderListener, mHandler);
 
-        // create an ImageReader for the reprocess capture
-        mSecondImageReaderListener = new SimpleImageReaderListener();
-        mSecondImageReader = makeImageReader(reprocessOutputSize, reprocessOutputFormat,
-                MAX_NUM_IMAGE_READER_IMAGES, mSecondImageReaderListener, mHandler);
+        if (!mShareOneImageReader) {
+            // create an ImageReader for the reprocess capture
+            mSecondImageReaderListener = new SimpleImageReaderListener();
+            mSecondImageReader = makeImageReader(reprocessOutputSize, reprocessOutputFormat,
+                    maxImages, mSecondImageReaderListener, mHandler);
+        }
     }
 
+    /**
+     * Close two image readers.
+     */
     private void closeImageReaders() {
         CameraTestUtils.closeImageReader(mFirstImageReader);
         mFirstImageReader = null;
@@ -423,17 +634,31 @@
         mSecondImageReader = null;
     }
 
-    private void setupReprocessibleSession(Surface previewSurface) throws Exception {
+    /**
+     * Set up a reprocessible session and create an ImageWriter with the sessoin's input surface.
+     */
+    private void setupReprocessibleSession(Surface previewSurface, int numImageWriterImages)
+            throws Exception {
         // create a reprocessible capture session
         List<Surface> outSurfaces = new ArrayList<Surface>();
         outSurfaces.add(mFirstImageReader.getSurface());
-        outSurfaces.add(mSecondImageReader.getSurface());
+        if (!mShareOneImageReader) {
+            outSurfaces.add(mSecondImageReader.getSurface());
+        }
         if (previewSurface != null) {
             outSurfaces.add(previewSurface);
         }
 
         InputConfiguration inputConfig = new InputConfiguration(mFirstImageReader.getWidth(),
                 mFirstImageReader.getHeight(), mFirstImageReader.getImageFormat());
+        assertTrue(String.format("inputConfig is wrong: %dx%d format %d. Expect %dx%d format %d",
+                inputConfig.getWidth(), inputConfig.getHeight(), inputConfig.getFormat(),
+                mFirstImageReader.getWidth(), mFirstImageReader.getHeight(),
+                mFirstImageReader.getImageFormat()),
+                inputConfig.getWidth() == mFirstImageReader.getWidth() &&
+                inputConfig.getHeight() == mFirstImageReader.getHeight() &&
+                inputConfig.getFormat() == mFirstImageReader.getImageFormat());
+
         mSessionListener = new BlockingSessionCallback();
         mSession = configureReprocessibleCameraSession(mCamera, inputConfig, outSurfaces,
                 mSessionListener, mHandler);
@@ -441,9 +666,15 @@
         // create an ImageWriter
         mInputSurface = mSession.getInputSurface();
         mImageWriter = ImageWriter.newInstance(mInputSurface,
-                MAX_NUM_IMAGE_WRITER_IMAGES);
+                numImageWriterImages);
+
+        mImageWriterListener = new SimpleImageWriterListener(mImageWriter);
+        mImageWriter.setImageListener(mImageWriterListener, mHandler);
     }
 
+    /**
+     * Close the reprocessible session and ImageWriter.
+     */
     private void closeReprossibleSession() {
         mInputSurface = null;
 
@@ -458,20 +689,74 @@
         }
     }
 
-    private Image doReprocessCapture() throws Exception {
-        // issue and wait on regular capture request
-        TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
-                /*inputResult*/null);
-        Image image = mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+    /**
+     * Do one reprocess capture.
+     */
+    private ImageResultHolder doReprocessCapture() throws Exception {
+        return doReprocessBurstCapture(/*numBurst*/1)[0];
+    }
 
-        // queue the image to image writer
-        mImageWriter.queueInputImage(image);
+    /**
+     * Do a burst of reprocess captures.
+     */
+    private ImageResultHolder[] doReprocessBurstCapture(int numBurst) throws Exception {
+        boolean[] isReprocessCaptures = new boolean[numBurst];
+        for (int i = 0; i < numBurst; i++) {
+            isReprocessCaptures[i] = true;
+        }
 
-        // issue and wait on reprocess capture request
-        TotalCaptureResult reprocessResult =
-                submitCaptureRequest(mSecondImageReader.getSurface(), result);
+        return doMixedReprocessBurstCapture(isReprocessCaptures);
+    }
 
-        return mSecondImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+    /**
+     * Do a burst of captures that are mixed with regular and reprocess captures.
+     *
+     * @param isReprocessCaptures An array whose elements indicate whether it's a reprocess capture
+     *                            request. If the element is true, it represents a reprocess capture
+     *                            request. If the element is false, it represents a regular capture
+     *                            request. The size of the array is the number of capture requests
+     *                            in the burst.
+     */
+    private ImageResultHolder[] doMixedReprocessBurstCapture(boolean[] isReprocessCaptures)
+            throws Exception {
+        if (isReprocessCaptures == null || isReprocessCaptures.length <= 0) {
+            throw new IllegalArgumentException("isReprocessCaptures must have at least 1 capture.");
+        }
+
+        TotalCaptureResult[] results = new TotalCaptureResult[isReprocessCaptures.length];
+        for (int i = 0; i < isReprocessCaptures.length; i++) {
+            // submit a capture and get the result if this entry is a reprocess capture.
+            if (isReprocessCaptures[i]) {
+                results[i] = submitCaptureRequest(mFirstImageReader.getSurface(),
+                        /*inputResult*/null);
+                mImageWriter.queueInputImage(
+                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
+            }
+        }
+
+        Surface[] outputSurfaces = new Surface[isReprocessCaptures.length];
+        for (int i = 0; i < isReprocessCaptures.length; i++) {
+            if (mShareOneImageReader) {
+                outputSurfaces[i] = mFirstImageReader.getSurface();
+            } else {
+                outputSurfaces[i] = mSecondImageReader.getSurface();
+            }
+        }
+
+        TotalCaptureResult[] finalResults = submitMixedCaptureBurstRequest(outputSurfaces, results);
+
+        ImageResultHolder[] holders = new ImageResultHolder[isReprocessCaptures.length];
+        for (int i = 0; i < isReprocessCaptures.length; i++) {
+            Image image;
+            if (mShareOneImageReader) {
+                image = mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+            } else {
+                image = mSecondImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+            }
+            holders[i] = new ImageResultHolder(image, finalResults[i]);
+        }
+
+        return holders;
     }
 
     /**
@@ -487,25 +772,85 @@
      * Issue a capture request and return the result. If inputResult is null, it's a regular
      * request. Otherwise, it's a reprocess request.
      */
-    private TotalCaptureResult submitCaptureRequest(Surface output, TotalCaptureResult inputResult)
-            throws Exception {
-        SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
-        CaptureRequest.Builder builder;
-        boolean isReprocess = inputResult != null;
-        if (isReprocess) {
-            builder = mCamera.createReprocessCaptureRequest(inputResult);
-        } else {
-            builder = mCamera.createCaptureRequest(CAPTURE_TEMPLATE);
+    private TotalCaptureResult submitCaptureRequest(Surface output,
+            TotalCaptureResult inputResult) throws Exception {
+        Surface[] outputs = new Surface[1];
+        outputs[0] = output;
+        TotalCaptureResult[] inputResults = new TotalCaptureResult[1];
+        inputResults[0] = inputResult;
+
+        return submitMixedCaptureBurstRequest(outputs, inputResults)[0];
+    }
+
+    /**
+     * Submit a burst request mixed with regular and reprocess requests.
+     *
+     * @param outputs An array of output surfaces. One output surface will be used in one request
+     *                so the length of the array is the number of requests in a burst request.
+     * @param inputResults An array of input results. If it's null, all requests are regular
+     *                     requests. If an element is null, that element represents a regular
+     *                     request. If an element if not null, that element represents a reprocess
+     *                     request.
+     *
+     */
+    private TotalCaptureResult[] submitMixedCaptureBurstRequest(Surface[] outputs,
+            TotalCaptureResult[] inputResults) throws Exception {
+        if (outputs == null || outputs.length <= 0) {
+            throw new IllegalArgumentException("outputs must have at least 1 surface");
+        } else if (inputResults != null && inputResults.length != outputs.length) {
+            throw new IllegalArgumentException("The lengths of outputs and inputResults " +
+                    "don't match");
         }
 
-        builder.addTarget(output);
-        CaptureRequest request = builder.build();
-        assertTrue("Capture request reprocess type " + request.isReprocess() + " is wrong.",
-                request.isReprocess() == isReprocess);
-        mSession.capture(request, captureCallback, mHandler);
+        int numReprocessCaptures = 0;
+        SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
+        ArrayList<CaptureRequest> captureRequests = new ArrayList<>(outputs.length);
 
-        // wait for regular capture result
-        return captureCallback.getTotalCaptureResultForRequest(request, CAPTURE_TIMEOUT_FRAMES);
+        // Prepare a list of capture requests. Whether it's a regular or reprocess capture request
+        // is based on inputResults array.
+        for (int i = 0; i < outputs.length; i++) {
+            CaptureRequest.Builder builder;
+            boolean isReprocess = (inputResults != null && inputResults[i] != null);
+            if (isReprocess) {
+                builder = mCamera.createReprocessCaptureRequest(inputResults[i]);
+                numReprocessCaptures++;
+            } else {
+                builder = mCamera.createCaptureRequest(CAPTURE_TEMPLATE);
+            }
+            builder.addTarget(outputs[i]);
+            CaptureRequest request = builder.build();
+            assertTrue("Capture request reprocess type " + request.isReprocess() + " is wrong.",
+                request.isReprocess() == isReprocess);
+
+            captureRequests.add(request);
+        }
+
+        if (captureRequests.size() == 1) {
+            mSession.capture(captureRequests.get(0), captureCallback, mHandler);
+        } else {
+            mSession.captureBurst(captureRequests, captureCallback, mHandler);
+        }
+
+        TotalCaptureResult[] results;
+        if (numReprocessCaptures == 0 || numReprocessCaptures == outputs.length) {
+            results = new TotalCaptureResult[outputs.length];
+            // If the requests are not mixed, they should come in order.
+            for (int i = 0; i < results.length; i++){
+                results[i] = captureCallback.getTotalCaptureResultForRequest(
+                        captureRequests.get(i), CAPTURE_TIMEOUT_FRAMES);
+            }
+        } else {
+            // If the requests are mixed, they may not come in order.
+            results = captureCallback.getTotalCaptureResultsForRequests(
+                    captureRequests, CAPTURE_TIMEOUT_FRAMES * captureRequests.size());
+        }
+
+        // make sure all input surfaces are released.
+        for (int i = 0; i < numReprocessCaptures; i++) {
+            mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
+        }
+
+        return results;
     }
 
     private Size getMaxSize(int format, StaticMetadata.StreamDirection direction) {
@@ -520,4 +865,45 @@
     private boolean isOpaqueReprocessSupported(String cameraId) throws Exception {
         return isReprocessSupported(cameraId, ImageFormat.PRIVATE);
     }
-}
\ No newline at end of file
+
+    private void dumpImage(Image image, String name) {
+        String filename = DEBUG_FILE_NAME_BASE + name;
+        switch(image.getFormat()) {
+            case ImageFormat.JPEG:
+                filename += ".jpg";
+                break;
+            case ImageFormat.NV16:
+            case ImageFormat.NV21:
+            case ImageFormat.YUV_420_888:
+                filename += ".yuv";
+                break;
+            default:
+                filename += "." + image.getFormat();
+                break;
+        }
+
+        Log.d(TAG, "dumping an image to " + filename);
+        dumpFile(filename , getDataFromImage(image));
+    }
+
+    /**
+     * A class that holds an Image and a TotalCaptureResult.
+     */
+    private static class ImageResultHolder {
+        private final Image mImage;
+        private final TotalCaptureResult mResult;
+
+        public ImageResultHolder(Image image, TotalCaptureResult result) {
+            mImage = image;
+            mResult = result;
+        }
+
+        public Image getImage() {
+            return mImage;
+        }
+
+        public TotalCaptureResult getTotalCaptureResult() {
+            return mResult;
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraErrorCollector.java b/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraErrorCollector.java
index 0ee5ffc..9f0c012 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraErrorCollector.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraErrorCollector.java
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CaptureRequest.Builder;
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.params.MeteringRectangle;
+import android.media.Image;
 import android.util.Log;
 import android.util.Size;
 
@@ -1049,4 +1050,13 @@
         Set<T> sizeSet = new HashSet<T>(list);
         expectTrue(msg + " each element must be distinct", sizeSet.size() == list.size());
     }
+
+    public void expectImageProperties(String msg, Image image, int format, Size size,
+            long timestampNs) {
+        expectEquals(msg + "Image format is wrong.", image.getFormat(), format);
+        expectEquals(msg + "Image width is wrong.", image.getWidth(), size.getWidth());
+        expectEquals(msg + "Image height is wrong.", image.getHeight(), size.getHeight());
+        expectEquals(msg + "Image timestamp is wrong.", image.getTimestamp(), timestampNs);
+    }
+
 }
diff --git a/tests/tests/media/assets/fileSequence0.ts b/tests/tests/media/assets/fileSequence0.ts
new file mode 100644
index 0000000..48f2bcd
--- /dev/null
+++ b/tests/tests/media/assets/fileSequence0.ts
Binary files differ
diff --git a/tests/tests/media/assets/fileSequence1.ts b/tests/tests/media/assets/fileSequence1.ts
new file mode 100644
index 0000000..737fbd0
--- /dev/null
+++ b/tests/tests/media/assets/fileSequence1.ts
Binary files differ
diff --git a/tests/tests/media/assets/prog_index.m3u8 b/tests/tests/media/assets/prog_index.m3u8
new file mode 100644
index 0000000..88f99d3
--- /dev/null
+++ b/tests/tests/media/assets/prog_index.m3u8
@@ -0,0 +1,10 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:9.90000,
+fileSequence0.ts
+#EXTINF:10.00000,
+fileSequence1.ts
+#EXT-X-ENDLIST
diff --git a/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java b/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
index dd7c1f6..e10d71c 100644
--- a/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
+++ b/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
@@ -18,12 +18,16 @@
 import android.cts.util.MediaUtils;
 import android.media.MediaFormat;
 import android.media.MediaPlayer;
+import android.media.MediaPlayer.TrackInfo;
+import android.media.TimedMetaData;
 import android.os.Looper;
+import android.os.PowerManager;
 import android.os.SystemClock;
 import android.util.Log;
 import android.webkit.cts.CtsTestServer;
 
 import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Tests of MediaPlayer streaming capabilities.
@@ -307,6 +311,94 @@
         localHlsTest("hls.m3u8", false, true);
     }
 
+    public void testPlayHlsStreamWithTimedId3() throws Throwable {
+        mServer = new CtsTestServer(mContext);
+        try {
+            // counter must be final if we want to access it inside onTimedMetaData;
+            // use AtomicInteger so we can have a final counter object with mutable integer value.
+            final AtomicInteger counter = new AtomicInteger();
+            String stream_url = mServer.getAssetUrl("prog_index.m3u8");
+            mMediaPlayer.setDataSource(stream_url);
+            mMediaPlayer.setDisplay(getActivity().getSurfaceHolder());
+            mMediaPlayer.setScreenOnWhilePlaying(true);
+            mMediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
+            mMediaPlayer.setOnTimedMetaDataListener(new MediaPlayer.OnTimedMetaDataListener() {
+                @Override
+                public void onTimedMetaData(MediaPlayer mp, TimedMetaData md) {
+                    counter.incrementAndGet();
+                    int pos = mp.getCurrentPosition();
+                    long timeUs = md.getTimeUs();
+                    byte[] rawData = md.getRawData();
+                    // Raw data contains an id3 tag holding the decimal string representation of
+                    // the associated time stamp rounded to the closest half second.
+
+                    int offset = 0;
+                    offset += 3; // "ID3"
+                    offset += 2; // version
+                    offset += 1; // flags
+                    offset += 4; // size
+                    offset += 4; // "TXXX"
+                    offset += 4; // frame size
+                    offset += 2; // frame flags
+                    offset += 1; // "\x03" : UTF-8 encoded Unicode
+                    offset += 1; // "\x00" : null-terminated empty description
+
+                    int length = rawData.length;
+                    length -= offset;
+                    length -= 1; // "\x00" : terminating null
+
+                    String data = new String(rawData, offset, length);
+                    int dataTimeUs = Integer.parseInt(data);
+                    assertTrue("Timed ID3 timestamp does not match content",
+                            Math.abs(dataTimeUs - timeUs) < 500000);
+                    assertTrue("Timed ID3 arrives after timestamp", pos * 1000 < timeUs);
+                }
+            });
+
+            final Object completion = new Object();
+            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+                int run;
+                @Override
+                public void onCompletion(MediaPlayer mp) {
+                    if (run++ == 0) {
+                        mMediaPlayer.seekTo(0);
+                        mMediaPlayer.start();
+                    } else {
+                        mMediaPlayer.stop();
+                        synchronized (completion) {
+                            completion.notify();
+                        }
+                    }
+                }
+            });
+
+            mMediaPlayer.prepare();
+            mMediaPlayer.start();
+            assertTrue("MediaPlayer not playing", mMediaPlayer.isPlaying());
+
+            int i = -1;
+            TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+            for (i = 0; i < trackInfos.length; i++) {
+                TrackInfo trackInfo = trackInfos[i];
+                if (trackInfo.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_METADATA) {
+                    break;
+                }
+            }
+            assertTrue("Stream has no timed ID3 track", i >= 0);
+            mMediaPlayer.selectTrack(i);
+
+            synchronized (completion) {
+                completion.wait();
+            }
+
+            // There are a total of 19 metadata access units in the test stream; every one of them
+            // should be received twice: once before the seek and once after.
+            assertTrue("Incorrect number of timed ID3s recieved", counter.get() == 38);
+        } finally {
+            mServer.shutdown();
+        }
+    }
+
     private static class WorkerWithPlayer implements Runnable {
         private final Object mLock = new Object();
         private Looper mLooper;
diff --git a/tests/tests/renderscript/src/android/renderscript/cts/IntrinsicBLAS.java b/tests/tests/renderscript/src/android/renderscript/cts/IntrinsicBLAS.java
index ff5bf84..f6b3176 100644
--- a/tests/tests/renderscript/src/android/renderscript/cts/IntrinsicBLAS.java
+++ b/tests/tests/renderscript/src/android/renderscript/cts/IntrinsicBLAS.java
@@ -1999,14 +1999,8 @@
             if (cM != cN) {
                 return false;
             }
-            if (TransA != ScriptIntrinsicBLAS.NO_TRANSPOSE) {
-                if (aN != cM) {
-                    return false;
-                }
-            } else {
-                if (aM != cM) {
-                    return false;
-                }
+            if (aM != cM) {
+                return false;
             }
         } else if (A != null && B != null) {
             // A and B only
diff --git a/tests/tests/renderscript/src/android/renderscript/cts/rsAllocationCopyTest.java b/tests/tests/renderscript/src/android/renderscript/cts/rsAllocationCopyTest.java
new file mode 100644
index 0000000..f74fa38
--- /dev/null
+++ b/tests/tests/renderscript/src/android/renderscript/cts/rsAllocationCopyTest.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2015 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.renderscript.cts;
+
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.Type;
+import java.util.Random;
+import android.util.Log;
+
+public class rsAllocationCopyTest extends RSBaseCompute {
+
+    public void test_rsAllocationCopy1D_Byte() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(512);
+        int arr_len = width;
+        int offset = random.nextInt(arr_len);
+        int count = random.nextInt(arr_len - offset);
+
+        byte[] inArray = new byte[arr_len];
+        byte[] outArray = new byte[arr_len];
+        random.nextBytes(inArray);
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I8(mRS));
+        typeBuilder.setX(width);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn1D(aIn);
+        s.set_aOut1D(aOut);
+        s.set_xOff(offset);
+        s.set_xCount(count);
+        s.invoke_test1D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < arr_len; i++) {
+            if (offset <= i && i < offset + count) {
+                if (inArray[i] != outArray[i]) {
+                    result = false;
+                    break;
+                }
+            } else {
+                if (outArray[i] != 0) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy1D_Byte failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy1D_Short() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(512);
+        int arr_len = width;
+        int offset = random.nextInt(arr_len);
+        int count = random.nextInt(arr_len - offset);
+
+        short[] inArray = new short[arr_len];
+        short[] outArray = new short[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = (short)random.nextInt();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I16(mRS));
+        typeBuilder.setX(width);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn1D(aIn);
+        s.set_aOut1D(aOut);
+        s.set_xOff(offset);
+        s.set_xCount(count);
+        s.invoke_test1D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < arr_len; i++) {
+            if (offset <= i && i < offset + count) {
+                if (inArray[i] != outArray[i]) {
+                    result = false;
+                    break;
+                }
+            } else {
+                if (outArray[i] != 0) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy1D_Short failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy1D_Int() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(512);
+        int arr_len = width;
+        int offset = random.nextInt(arr_len);
+        int count = random.nextInt(arr_len - offset);
+
+        int[] inArray = new int[arr_len];
+        int[] outArray = new int[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextInt();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I32(mRS));
+        typeBuilder.setX(width);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn1D(aIn);
+        s.set_aOut1D(aOut);
+        s.set_xOff(offset);
+        s.set_xCount(count);
+        s.invoke_test1D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < arr_len; i++) {
+            if (offset <= i && i < offset + count) {
+                if (inArray[i] != outArray[i]) {
+                    result = false;
+                    break;
+                }
+            } else {
+                if (outArray[i] != 0) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy1D_Int failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy1D_Float() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(512);
+        int arr_len = width;
+        int offset = random.nextInt(arr_len);
+        int count = random.nextInt(arr_len - offset);
+
+        float[] inArray = new float[arr_len];
+        float[] outArray = new float[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextFloat();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.F32(mRS));
+        typeBuilder.setX(width);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn1D(aIn);
+        s.set_aOut1D(aOut);
+        s.set_xOff(offset);
+        s.set_xCount(count);
+        s.invoke_test1D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+
+        boolean result = true;
+        for (int i = 0; i < arr_len; i++) {
+            if (offset <= i && i < offset + count) {
+                if (inArray[i] != outArray[i]) {
+                    result = false;
+                    break;
+                }
+            } else {
+                if (outArray[i] != 0) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy1D_Float failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy1D_Long() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(512);
+        int arr_len = width;
+        int offset = random.nextInt(arr_len);
+        int count = random.nextInt(arr_len - offset);
+
+        long[] inArray = new long[arr_len];
+        long[] outArray = new long[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextLong();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I64(mRS));
+        typeBuilder.setX(width);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn1D(aIn);
+        s.set_aOut1D(aOut);
+        s.set_xOff(offset);
+        s.set_xCount(count);
+        s.invoke_test1D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < arr_len; i++) {
+            if (offset <= i && i < offset + count) {
+                if (inArray[i] != outArray[i]) {
+                    result = false;
+                    break;
+                }
+            } else {
+                if (outArray[i] != 0) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy1D_Long failed, output array does not match input",
+                   result);
+    }
+
+
+    public void test_rsAllocationCopy2D_Byte() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(128);
+        int height = random.nextInt(128);
+        int xOff = random.nextInt(width);
+        int yOff = random.nextInt(height);
+        int xCount = random.nextInt(width - xOff);
+        int yCount = random.nextInt(height - yOff);
+        int arr_len = width * height;
+
+        byte[] inArray = new byte[arr_len];
+        byte[] outArray = new byte[arr_len];
+        random.nextBytes(inArray);
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I8(mRS));
+        typeBuilder.setX(width).setY(height);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn2D(aIn);
+        s.set_aOut2D(aOut);
+        s.set_xOff(xOff);
+        s.set_yOff(yOff);
+        s.set_xCount(xCount);
+        s.set_yCount(yCount);
+        s.invoke_test2D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < height; i++) {
+            for (int j = 0; j < width; j++) {
+                int pos = i * width + j;
+                if (yOff <= i && i < yOff + yCount &&
+                    xOff <= j && j < xOff + xCount) {
+                    if (inArray[pos] != outArray[pos]) {
+                        result = false;
+                        break;
+                    }
+                } else {
+                    if (outArray[pos] != 0) {
+                        result = false;
+                        break;
+                    }
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy2D_Byte failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy2D_Short() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(128);
+        int height = random.nextInt(128);
+        int xOff = random.nextInt(width);
+        int yOff = random.nextInt(height);
+        int xCount = random.nextInt(width - xOff);
+        int yCount = random.nextInt(height - yOff);
+        int arr_len = width * height;
+
+        short[] inArray = new short[arr_len];
+        short[] outArray = new short[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = (short)random.nextInt();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I16(mRS));
+        typeBuilder.setX(width).setY(height);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn2D(aIn);
+        s.set_aOut2D(aOut);
+        s.set_xOff(xOff);
+        s.set_yOff(yOff);
+        s.set_xCount(xCount);
+        s.set_yCount(yCount);
+        s.invoke_test2D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < height; i++) {
+            for (int j = 0; j < width; j++) {
+                int pos = i * width + j;
+                if (yOff <= i && i < yOff + yCount &&
+                    xOff <= j && j < xOff + xCount) {
+                    if (inArray[pos] != outArray[pos]) {
+                        result = false;
+                        break;
+                    }
+                } else {
+                    if (outArray[pos] != 0) {
+                        result = false;
+                        break;
+                    }
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy2D_Short failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy2D_Int() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(128);
+        int height = random.nextInt(128);
+        int xOff = random.nextInt(width);
+        int yOff = random.nextInt(height);
+        int xCount = random.nextInt(width - xOff);
+        int yCount = random.nextInt(height - yOff);
+        int arr_len = width * height;
+
+        int[] inArray = new int[arr_len];
+        int[] outArray = new int[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextInt();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I32(mRS));
+        typeBuilder.setX(width).setY(height);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn2D(aIn);
+        s.set_aOut2D(aOut);
+        s.set_xOff(xOff);
+        s.set_yOff(yOff);
+        s.set_xCount(xCount);
+        s.set_yCount(yCount);
+        s.invoke_test2D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < height; i++) {
+            for (int j = 0; j < width; j++) {
+                int pos = i * width + j;
+                if (yOff <= i && i < yOff + yCount &&
+                    xOff <= j && j < xOff + xCount) {
+                    if (inArray[pos] != outArray[pos]) {
+                        result = false;
+                        break;
+                    }
+                } else {
+                    if (outArray[pos] != 0) {
+                        result = false;
+                        break;
+                    }
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy2D_Int failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy2D_Float() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(128);
+        int height = random.nextInt(128);
+        int xOff = random.nextInt(width);
+        int yOff = random.nextInt(height);
+        int xCount = random.nextInt(width - xOff);
+        int yCount = random.nextInt(height - yOff);
+        int arr_len = width * height;
+
+        float[] inArray = new float[arr_len];
+        float[] outArray = new float[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextFloat();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.F32(mRS));
+        typeBuilder.setX(width).setY(height);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn2D(aIn);
+        s.set_aOut2D(aOut);
+        s.set_xOff(xOff);
+        s.set_yOff(yOff);
+        s.set_xCount(xCount);
+        s.set_yCount(yCount);
+        s.invoke_test2D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < height; i++) {
+            for (int j = 0; j < width; j++) {
+                int pos = i * width + j;
+                if (yOff <= i && i < yOff + yCount &&
+                    xOff <= j && j < xOff + xCount) {
+                    if (inArray[pos] != outArray[pos]) {
+                        result = false;
+                        break;
+                    }
+                } else {
+                    if (outArray[pos] != 0) {
+                        result = false;
+                        break;
+                    }
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy2D_Float failed, output array does not match input",
+                   result);
+    }
+
+    public void test_rsAllocationCopy2D_Long() {
+        Random random = new Random(0x172d8ab9);
+        int width = random.nextInt(128);
+        int height = random.nextInt(128);
+        int xOff = random.nextInt(width);
+        int yOff = random.nextInt(height);
+        int xCount = random.nextInt(width - xOff);
+        int yCount = random.nextInt(height - yOff);
+        int arr_len = width * height;
+
+        long[] inArray = new long[arr_len];
+        long[] outArray = new long[arr_len];
+        for (int i = 0; i < arr_len; i++) {
+            inArray[i] = random.nextLong();
+        }
+
+        Type.Builder typeBuilder = new Type.Builder(mRS, Element.I64(mRS));
+        typeBuilder.setX(width).setY(height);
+        Allocation aIn = Allocation.createTyped(mRS, typeBuilder.create());
+        Allocation aOut = Allocation.createTyped(mRS, typeBuilder.create());
+        aIn.copyFrom(inArray);
+        aOut.copyFrom(outArray);
+
+        ScriptC_rsallocationcopy s = new ScriptC_rsallocationcopy(mRS);
+        s.set_aIn2D(aIn);
+        s.set_aOut2D(aOut);
+        s.set_xOff(xOff);
+        s.set_yOff(yOff);
+        s.set_xCount(xCount);
+        s.set_yCount(yCount);
+        s.invoke_test2D();
+        mRS.finish();
+        aOut.copyTo(outArray);
+
+        boolean result = true;
+        for (int i = 0; i < height; i++) {
+            for (int j = 0; j < width; j++) {
+                int pos = i * width + j;
+                if (yOff <= i && i < yOff + yCount &&
+                    xOff <= j && j < xOff + xCount) {
+                    if (inArray[pos] != outArray[pos]) {
+                        result = false;
+                        break;
+                    }
+                } else {
+                    if (outArray[pos] != 0) {
+                        result = false;
+                        break;
+                    }
+                }
+            }
+        }
+        assertTrue("test_rsAllocationCopy2D_Long failed, output array does not match input",
+                   result);
+    }
+}
diff --git a/tests/tests/renderscript/src/android/renderscript/cts/rsallocationcopy.rs b/tests/tests/renderscript/src/android/renderscript/cts/rsallocationcopy.rs
new file mode 100644
index 0000000..4d76493
--- /dev/null
+++ b/tests/tests/renderscript/src/android/renderscript/cts/rsallocationcopy.rs
@@ -0,0 +1,19 @@
+#include "shared.rsh"
+
+rs_allocation aIn1D;
+rs_allocation aOut1D;
+rs_allocation aIn2D;
+rs_allocation aOut2D;
+
+int xOff = 0;
+int yOff = 0;
+int xCount = 0;
+int yCount = 0;
+
+void test1D() {
+    rsAllocationCopy1DRange(aOut1D, xOff, 0, xCount, aIn1D, xOff, 0);
+}
+
+void test2D() {
+    rsAllocationCopy2DRange(aOut2D, xOff, yOff, 0, 0, xCount, yCount, aIn2D, xOff, yOff, 0, 0);
+}
diff --git a/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java b/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java
new file mode 100644
index 0000000..e393bb6
--- /dev/null
+++ b/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 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.webkit.cts;
+
+import android.cts.util.NullWebViewUtils;
+import android.cts.util.PollingCheck;
+import android.net.Uri;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.UiThreadTest;
+import android.webkit.WebMessage;
+import android.webkit.WebMessagePort;
+import android.webkit.WebView;
+
+import java.util.concurrent.CountDownLatch;
+import junit.framework.Assert;
+
+public class PostMessageTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+    public static final long TIMEOUT = 20000L;
+
+    private WebView mWebView;
+    private WebViewOnUiThread mOnUiThread;
+
+    private static final String WEBVIEW_MESSAGE = "from_webview";
+    private static final String BASE_URI = "http://www.example.com";
+
+    public PostMessageTest() {
+        super("com.android.cts.webkit", WebViewCtsActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final WebViewCtsActivity activity = getActivity();
+        mWebView = activity.getWebView();
+        if (mWebView != null) {
+            mOnUiThread = new WebViewOnUiThread(this, mWebView);
+            mOnUiThread.getSettings().setJavaScriptEnabled(true);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mOnUiThread != null) {
+            mOnUiThread.cleanUp();
+        }
+        super.tearDown();
+    }
+
+    private static final String TITLE_FROM_POST_MESSAGE =
+            "<!DOCTYPE html><html><body>"
+            + "    <script>"
+            + "        var received = '';"
+            + "        onmessage = function (e) {"
+            + "            received += e.data;"
+            + "            document.title = received; };"
+            + "    </script>"
+            + "</body></html>";
+
+    // Acks each received message from the message channel with a seq number.
+    private static final String CHANNEL_MESSAGE =
+            "<!DOCTYPE html><html><body>"
+            + "    <script>"
+            + "        var counter = 0;"
+            + "        onmessage = function (e) {"
+            + "            var myPort = e.ports[0];"
+            + "            myPort.onmessage = function (f) {"
+            + "                myPort.postMessage(f.data + counter++);"
+            + "            }"
+            + "        }"
+            + "   </script>"
+            + "</body></html>";
+
+    private void loadPage(String data) {
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(BASE_URI, data,
+                "text/html", "UTF-8", null);
+    }
+
+    private void waitForTitle(final String title) {
+        new PollingCheck(TIMEOUT) {
+            @Override
+            protected boolean check() {
+                return mOnUiThread.getTitle().equals(title);
+            }
+        }.run();
+    }
+
+    // Post a string message to main frame and make sure it is received.
+    public void testSimpleMessageToMainFrame() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        loadPage(TITLE_FROM_POST_MESSAGE);
+        WebMessage message = new WebMessage(WEBVIEW_MESSAGE);
+        mOnUiThread.postMessageToMainFrame(message, Uri.parse(BASE_URI));
+        waitForTitle(WEBVIEW_MESSAGE);
+    }
+
+    // Post multiple messages to main frame and make sure they are received in
+    // correct order.
+    public void testMultipleMessagesToMainFrame() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        loadPage(TITLE_FROM_POST_MESSAGE);
+        for (int i = 0; i < 10; i++) {
+            mOnUiThread.postMessageToMainFrame(new WebMessage(Integer.toString(i)),
+                    Uri.parse(BASE_URI));
+        }
+        waitForTitle("0123456789");
+    }
+
+    // Create a message channel and make sure it can be used for data transfer to/from js.
+    public void testMessageChannel() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        loadPage(CHANNEL_MESSAGE);
+        final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
+        WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
+        mOnUiThread.postMessageToMainFrame(message, Uri.parse(BASE_URI));
+        final int messageCount = 3;
+        final CountDownLatch latch = new CountDownLatch(messageCount);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                for (int i = 0; i < messageCount; i++) {
+                    channel[0].postMessage(new WebMessage(WEBVIEW_MESSAGE + i));
+                }
+                channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+                    @Override
+                    public void onMessage(WebMessagePort port, WebMessage message) {
+                        int i = messageCount - (int)latch.getCount();
+                        assertEquals(WEBVIEW_MESSAGE + i + i, message.getData());
+                        latch.countDown();
+                    }
+                });
+            }
+        });
+        // Wait for all the responses to arrive.
+        boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
+    }
+
+    // Test that a message port that is closed cannot used to send a message
+    public void testClose() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        loadPage(CHANNEL_MESSAGE);
+        final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
+        WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
+        mOnUiThread.postMessageToMainFrame(message, Uri.parse(BASE_URI));
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    channel[0].close();
+                    channel[0].postMessage(new WebMessage(WEBVIEW_MESSAGE));
+                } catch (IllegalStateException ex) {
+                    // expect to receive an exception
+                    return;
+                }
+                Assert.fail("A closed port cannot be used to transfer messages");
+            }
+         });
+    }
+
+    // Sends a new message channel from JS to Java.
+    private static final String CHANNEL_FROM_JS =
+            "<!DOCTYPE html><html><body>"
+            + "    <script>"
+            + "        var counter = 0;"
+            + "        var mc = new MessageChannel();"
+            + "        var received = '';"
+            + "        mc.port1.onmessage = function (e) {"
+            + "               received = e.data;"
+            + "               document.title = e.data;"
+            + "        };"
+            + "        onmessage = function (e) {"
+            + "            var myPort = e.ports[0];"
+            + "            myPort.postMessage('', [mc.port2]);"
+            + "        };"
+            + "   </script>"
+            + "</body></html>";
+
+    // Test a message port created in JS can be received and used for message transfer.
+    public void testReceiveMessagePort() throws Throwable {
+        final String hello = "HELLO";
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        loadPage(CHANNEL_FROM_JS);
+        final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
+        WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
+        mOnUiThread.postMessageToMainFrame(message, Uri.parse(BASE_URI));
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+                    @Override
+                    public void onMessage(WebMessagePort port, WebMessage message) {
+                        message.getPorts()[0].postMessage(new WebMessage(hello));
+                    }
+                });
+            }
+        });
+        waitForTitle(hello);
+    }
+}