Cherry-pick SurfaceViewSyncTests

bug: 29242061
Change-Id: I4176f4b93953f8e9ead1c42b46240ce4c931e9cf
diff --git a/tests/tests/view/Android.mk b/tests/tests/view/Android.mk
index 6a8d49c..ba4be93 100644
--- a/tests/tests/view/Android.mk
+++ b/tests/tests/view/Android.mk
@@ -29,11 +29,15 @@
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    ctsdeviceutil ctstestrunner mockito-target
+    ctsdeviceutil \
+    ctstestrunner \
+    mockito-target \
+    ub-uiautomator \
+    android-support-test
 
 LOCAL_JNI_SHARED_LIBRARIES := libctsview_jni libnativehelper_compat_libc++
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-renderscript-files-under, src)
 
 LOCAL_PACKAGE_NAME := CtsViewTestCases
 
diff --git a/tests/tests/view/AndroidManifest.xml b/tests/tests/view/AndroidManifest.xml
index a98c447..526e85f 100644
--- a/tests/tests/view/AndroidManifest.xml
+++ b/tests/tests/view/AndroidManifest.xml
@@ -19,6 +19,8 @@
     package="android.view.cts">
 
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
     <application android:label="Android TestCase"
                 android:icon="@drawable/size_48x48"
                 android:maxRecents="1"
@@ -239,6 +241,22 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="android.view.cts.DragDropActivity"
+                  android:label="DragDropActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/WhiteBackgroundTheme">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/view/res/raw/colors_video.mp4 b/tests/tests/view/res/raw/colors_video.mp4
new file mode 100644
index 0000000..0bec670
--- /dev/null
+++ b/tests/tests/view/res/raw/colors_video.mp4
Binary files differ
diff --git a/tests/tests/view/res/values/styles.xml b/tests/tests/view/res/values/styles.xml
index 9de4abd..4979241 100644
--- a/tests/tests/view/res/values/styles.xml
+++ b/tests/tests/view/res/values/styles.xml
@@ -177,4 +177,13 @@
         <item name="android:windowSwipeToDismiss">false</item>
     </style>
 
+    <style name="WhiteBackgroundTheme" parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowOverscan">true</item>
+        <item name="android:fadingEdge">none</item>
+        <item name="android:windowBackground">@android:color/white</item>
+        <item name="android:windowContentTransitions">false</item>
+        <item name="android:windowAnimationStyle">@null</item>
+    </style>
 </resources>
diff --git a/tests/tests/view/src/android/view/cts/SurfaceViewSyncTests.java b/tests/tests/view/src/android/view/cts/SurfaceViewSyncTests.java
new file mode 100644
index 0000000..61c30a0
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/SurfaceViewSyncTests.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 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.view.cts;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.media.MediaPlayer;
+import android.os.Environment;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiSelector;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
+import android.view.cts.surfacevalidator.AnimationFactory;
+import android.view.cts.surfacevalidator.AnimationTestCase;
+import android.view.cts.surfacevalidator.CapturedActivity;
+import android.view.cts.surfacevalidator.ViewFactory;
+import android.widget.FrameLayout;
+
+import libcore.io.IoUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@SuppressLint("RtlHardcoded")
+public class SurfaceViewSyncTests {
+    private static final String TAG = "SurfaceViewSyncTests";
+    private static final int PERMISSION_DIALOG_WAIT_MS = 1000;
+
+    /**
+     * Want to be especially sure we don't leave up the permission dialog, so try and dismiss both
+     * before and after test.
+     */
+    @Before
+    @After
+    public void setUp() throws UiObjectNotFoundException {
+        // The permission dialog will be auto-opened by the activity - find it and accept
+        UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        UiSelector acceptButtonSelector = new UiSelector().resourceId("android:id/button1");
+        UiObject acceptButton = uiDevice.findObject(acceptButtonSelector);
+        if (acceptButton.waitForExists(PERMISSION_DIALOG_WAIT_MS)) {
+            boolean success = acceptButton.click();
+            Log.d(TAG, "found permission dialog, click attempt success = " + success);
+        }
+    }
+
+    private CapturedActivity getActivity() {
+        return (CapturedActivity) mActivityRule.getActivity();
+    }
+
+    private MediaPlayer getMediaPlayer() {
+        return getActivity().getMediaPlayer();
+    }
+
+    @Rule
+    public ActivityTestRule mActivityRule = new ActivityTestRule<>(CapturedActivity.class);
+
+    @Rule
+    public TestName mName = new TestName();
+
+    static ValueAnimator makeInfinite(ValueAnimator a) {
+        a.setRepeatMode(ObjectAnimator.REVERSE);
+        a.setRepeatCount(ObjectAnimator.INFINITE);
+        a.setDuration(200);
+        a.setInterpolator(new LinearInterpolator());
+        return a;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // ViewFactories
+    ///////////////////////////////////////////////////////////////////////////
+
+    private ViewFactory sEmptySurfaceViewFactory = SurfaceView::new;
+
+    private ViewFactory sGreenSurfaceViewFactory = context -> {
+        SurfaceView surfaceView = new SurfaceView(context);
+        surfaceView.getHolder().setFixedSize(640, 480);
+        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {}
+
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+                Canvas canvas = holder.lockCanvas();
+                canvas.drawColor(Color.GREEN);
+                holder.unlockCanvasAndPost(canvas);
+            }
+
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {}
+        });
+        return surfaceView;
+    };
+
+    private ViewFactory sVideoViewFactory = context -> {
+        SurfaceView surfaceView = new SurfaceView(context);
+        surfaceView.getHolder().setFixedSize(640, 480);
+        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                getMediaPlayer().setSurface(holder.getSurface());
+                getMediaPlayer().start();
+            }
+
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {
+                getMediaPlayer().pause();
+                getMediaPlayer().setSurface(null);
+            }
+        });
+        return surfaceView;
+    };
+
+    ///////////////////////////////////////////////////////////////////////////
+    // AnimationFactories
+    ///////////////////////////////////////////////////////////////////////////
+
+    private AnimationFactory sSmallScaleAnimationFactory = view -> {
+        view.setPivotX(0);
+        view.setPivotY(0);
+        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.SCALE_X, 0.01f, 1f);
+        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.01f, 1f);
+        return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY));
+    };
+
+    private AnimationFactory sBigScaleAnimationFactory = view -> {
+        view.setTranslationX(10);
+        view.setTranslationY(10);
+        view.setPivotX(0);
+        view.setPivotY(0);
+        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 3f);
+        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 3f);
+        return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY));
+    };
+
+    private AnimationFactory sTranslateAnimationFactory = view -> {
+        PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f);
+        PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f);
+        return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY));
+    };
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Bad frame capture
+    ///////////////////////////////////////////////////////////////////////////
+
+    private void saveFailureCaptures(SparseArray<Bitmap> failFrames) {
+        if (failFrames.size() == 0) return;
+
+        String directoryName = Environment.getExternalStorageDirectory()
+                + "/" + getClass().getSimpleName()
+                + "/" + mName.getMethodName();
+        File testDirectory = new File(directoryName);
+        if (testDirectory.exists()) {
+            String[] children = testDirectory.list();
+            if (children == null) {
+                return;
+            }
+            for (String file : children) {
+                new File(testDirectory, file).delete();
+            }
+        } else {
+            testDirectory.mkdirs();
+        }
+
+        for (int i = 0; i < failFrames.size(); i++) {
+            int frameNr = failFrames.keyAt(i);
+            Bitmap bitmap = failFrames.valueAt(i);
+
+            String bitmapName =  "frame_" + frameNr + ".png";
+            Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName);
+
+            File file = new File(directoryName, bitmapName);
+            FileOutputStream fileStream = null;
+            try {
+                fileStream = new FileOutputStream(file);
+                bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream);
+                fileStream.flush();
+            } catch (IOException e) {
+                e.printStackTrace();
+            } finally {
+                IoUtils.closeQuietly(fileStream);
+            }
+        }
+    }
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests
+    ///////////////////////////////////////////////////////////////////////////
+
+    public void verifyTest(AnimationTestCase testCase) throws InterruptedException {
+        CapturedActivity.TestResult result = getActivity().runTest(testCase);
+        saveFailureCaptures(result.failures);
+
+        float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames);
+        assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?",
+                failRatio < 0.95f);
+        assertTrue("Error: " + result.failFrames
+                + " incorrect frames observed - incorrect positioning",
+                result.failFrames == 0);
+        float framesPerSecond = 1.0f * result.passFrames
+                / TimeUnit.MILLISECONDS.toSeconds(CapturedActivity.CAPTURE_DURATION_MS);
+        assertTrue("Error, only " + result.passFrames
+                + " frames observed, virtual display only capturing at "
+                + framesPerSecond + " frames per second",
+                result.passFrames > 100);
+    }
+
+    /** Draws a moving 10x10 black rectangle, validates 100 pixels of black are seen each frame */
+    @Test
+    public void testSmallRect() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                context -> new View(context) {
+                    // draw a single pixel
+                    final Paint sBlackPaint = new Paint();
+                    @Override
+                    protected void onDraw(Canvas canvas) {
+                        canvas.drawRect(0, 0, 10, 10, sBlackPaint);
+                    }
+
+                    @SuppressWarnings("unused")
+                    void setOffset(int offset) {
+                        // Note: offset by integer values, to ensure no rounding
+                        // is done in rendering layer, as that may be brittle
+                        setTranslationX(offset);
+                        setTranslationY(offset);
+                    }
+                },
+                new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP),
+                view -> makeInfinite(ObjectAnimator.ofInt(view, "offset", 10, 30)),
+                (blackishPixelCount, width, height) ->
+                        blackishPixelCount >= 90 && blackishPixelCount <= 110));
+    }
+
+    /**
+     * Verifies that a SurfaceView without a surface is entirely black, with pixel count being
+     * approximate to avoid rounding brittleness.
+     */
+    @Test
+    public void testEmptySurfaceView() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sEmptySurfaceViewFactory,
+                new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP),
+                sTranslateAnimationFactory,
+                (blackishPixelCount, width, height) ->
+                        blackishPixelCount > 9000 && blackishPixelCount < 11000));
+    }
+
+    @Test
+    public void testSurfaceViewSmallScale() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sGreenSurfaceViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sSmallScaleAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+
+    @Test
+    public void testSurfaceViewBigScale() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sGreenSurfaceViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sBigScaleAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+
+    @Test
+    public void testVideoSurfaceViewTranslate() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sVideoViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sTranslateAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+
+    @Test
+    public void testVideoSurfaceViewRotated() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sVideoViewFactory,
+                new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP),
+                view -> makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view,
+                        PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f),
+                        PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f),
+                        PropertyValuesHolder.ofFloat(View.ROTATION, 45f, 45f))),
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+
+    @Test
+    public void testVideoSurfaceViewEdgeCoverage() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sVideoViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.CENTER),
+                view -> {
+                    ViewGroup parent = (ViewGroup) view.getParent();
+                    final int x = parent.getWidth() / 2;
+                    final int y = parent.getHeight() / 2;
+
+                    // Animate from left, to top, to right, to bottom
+                    return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view,
+                            PropertyValuesHolder.ofFloat(View.TRANSLATION_X, -x, 0, x, 0, -x),
+                            PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0, -y, 0, y, 0)));
+                },
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+
+    @Test
+    public void testVideoSurfaceViewCornerCoverage() throws InterruptedException {
+        verifyTest(new AnimationTestCase(
+                sVideoViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.CENTER),
+                view -> {
+                    ViewGroup parent = (ViewGroup) view.getParent();
+                    final int x = parent.getWidth() / 2;
+                    final int y = parent.getHeight() / 2;
+
+                    // Animate from top left, to top right, to bottom right, to bottom left
+                    return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view,
+                            PropertyValuesHolder.ofFloat(View.TRANSLATION_X, -x, x, x, -x, -x),
+                            PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -y, -y, y, y, -y)));
+                },
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationFactory.java b/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationFactory.java
new file mode 100644
index 0000000..c4c19cf
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+import android.animation.ValueAnimator;
+import android.view.View;
+
+public interface AnimationFactory {
+    ValueAnimator createAnimator(View view);
+}
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationTestCase.java b/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationTestCase.java
new file mode 100644
index 0000000..6b455e2
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/AnimationTestCase.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public class AnimationTestCase {
+    private final ViewFactory mViewFactory;
+    private final FrameLayout.LayoutParams mLayoutParams;
+    private final AnimationFactory mAnimationFactory;
+    private final PixelChecker mPixelChecker;
+
+    private FrameLayout mParent;
+    private ValueAnimator mAnimator;
+
+    public AnimationTestCase(ViewFactory viewFactory,
+            FrameLayout.LayoutParams layoutParams,
+            AnimationFactory animationFactory,
+            PixelChecker pixelChecker) {
+        mViewFactory = viewFactory;
+        mLayoutParams = layoutParams;
+        mAnimationFactory = animationFactory;
+        mPixelChecker = pixelChecker;
+    }
+
+    PixelChecker getChecker() {
+        return mPixelChecker;
+    }
+
+    public void start(Context context, FrameLayout parent) {
+        mParent = parent;
+        mParent.removeAllViews();
+        View view = mViewFactory.createView(context);
+        mParent.addView(view, mLayoutParams);
+        mAnimator = mAnimationFactory.createAnimator(view);
+        mAnimator.start();
+    }
+
+    public void end() {
+        mAnimator.cancel();
+        mParent.removeAllViews();
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/CapturedActivity.java b/tests/tests/view/src/android/view/cts/surfacevalidator/CapturedActivity.java
new file mode 100644
index 0000000..82d113d
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/CapturedActivity.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.MediaPlayer;
+import android.media.projection.MediaProjection;
+import android.media.projection.MediaProjectionManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import android.view.cts.R;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import static org.junit.Assert.*;
+
+
+public class CapturedActivity extends Activity {
+    public static class TestResult {
+        public int passFrames;
+        public int failFrames;
+        public final SparseArray<Bitmap> failures = new SparseArray<>();
+    }
+
+    private static final String TAG = "CapturedActivity";
+    private static final long TIME_OUT_MS = 25000;
+    private static final int PERMISSION_CODE = 1;
+    private MediaProjectionManager mProjectionManager;
+    private MediaProjection mMediaProjection;
+    private VirtualDisplay mVirtualDisplay;
+
+    private SurfacePixelValidator mSurfacePixelValidator;
+    private final Object mLock = new Object();
+
+    public static final long CAPTURE_DURATION_MS = 10000;
+
+    private static final long START_CAPTURE_DELAY_MS = 4000;
+    private static final long END_CAPTURE_DELAY_MS = START_CAPTURE_DELAY_MS + CAPTURE_DURATION_MS;
+    private static final long END_DELAY_MS = END_CAPTURE_DELAY_MS + 1000;
+
+    private MediaPlayer mMediaPlayer;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private volatile boolean mOnWatch;
+    private CountDownLatch mCountDownLatch;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mOnWatch = getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+        if (mOnWatch) {
+            // Don't try and set up test/capture infrastructure - they're not supported
+            return;
+        }
+
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
+
+        mProjectionManager =
+                (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+
+        mCountDownLatch = new CountDownLatch(1);
+        startActivityForResult(mProjectionManager.createScreenCaptureIntent(), PERMISSION_CODE);
+
+        mMediaPlayer = MediaPlayer.create(this, R.raw.colors_video);
+        mMediaPlayer.setLooping(true);
+    }
+
+    /**
+     * MediaPlayer pre-loaded with a video with no black pixels. Be kind, rewind.
+     */
+    public MediaPlayer getMediaPlayer() {
+        return mMediaPlayer;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        Log.d(TAG, "onDestroy");
+        if (mMediaProjection != null) {
+            mMediaProjection.stop();
+            mMediaProjection = null;
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (mOnWatch) return;
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
+
+        if (requestCode != PERMISSION_CODE) {
+            throw new IllegalStateException("Unknown request code: " + requestCode);
+        }
+        if (resultCode != RESULT_OK) {
+            throw new IllegalStateException("User denied screen sharing permission");
+        }
+        Log.d(TAG, "onActivityResult");
+        mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
+        mMediaProjection.registerCallback(new MediaProjectionCallback(), null);
+        mCountDownLatch.countDown();
+    }
+
+    public TestResult runTest(AnimationTestCase animationTestCase) throws InterruptedException {
+        TestResult testResult = new TestResult();
+        if (mOnWatch) {
+            /**
+             * Watch devices not supported, since they may not support:
+             *    1) displaying unmasked windows
+             *    2) RenderScript
+             *    3) Video playback
+             */
+            Log.d(TAG, "Skipping test on watch.");
+            testResult.passFrames = 1000;
+            testResult.failFrames = 0;
+            return testResult;
+        }
+
+        assertTrue("Can't initialize mediaProjection",
+                mCountDownLatch.await(TIME_OUT_MS, TimeUnit.MILLISECONDS));
+
+        mHandler.post(() -> {
+            Log.d(TAG, "Setting up test case");
+
+            // shouldn't be necessary, since we've already done this in #create,
+            // but ensure status/nav are hidden for test
+            getWindow().getDecorView().setSystemUiVisibility(
+                    View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
+
+            animationTestCase.start(getApplicationContext(),
+                    (FrameLayout) findViewById(android.R.id.content));
+        });
+
+        mHandler.postDelayed(() -> {
+            Log.d(TAG, "Starting capture");
+
+            Display display = getWindow().getDecorView().getDisplay();
+            Point size = new Point();
+            DisplayMetrics metrics = new DisplayMetrics();
+            display.getRealSize(size);
+            display.getMetrics(metrics);
+
+
+            mSurfacePixelValidator = new SurfacePixelValidator(CapturedActivity.this,
+                    size, animationTestCase.getChecker());
+            Log.d("MediaProjection", "Size is " + size.toString());
+            mVirtualDisplay = mMediaProjection.createVirtualDisplay("ScreenSharingDemo",
+                    size.x, size.y,
+                    metrics.densityDpi,
+                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+                    mSurfacePixelValidator.getSurface(),
+                    null /*Callbacks*/,
+                    null /*Handler*/);
+        }, START_CAPTURE_DELAY_MS);
+
+        mHandler.postDelayed(() -> {
+            Log.d(TAG, "Stopping capture");
+            mVirtualDisplay.release();
+            mVirtualDisplay = null;
+        }, END_CAPTURE_DELAY_MS);
+
+        mHandler.postDelayed(() -> {
+            Log.d(TAG, "Ending test case");
+            animationTestCase.end();
+            synchronized (mLock) {
+                mSurfacePixelValidator.finish(testResult);
+                mLock.notify();
+            }
+            mSurfacePixelValidator = null;
+        }, END_DELAY_MS);
+
+        synchronized (mLock) {
+            mLock.wait(TIME_OUT_MS);
+        }
+        Log.d(TAG, "Test finished, passFrames " + testResult.passFrames
+                + ", failFrames " + testResult.failFrames);
+        return testResult;
+    }
+
+    private class MediaProjectionCallback extends MediaProjection.Callback {
+        @Override
+        public void onStop() {
+            Log.d(TAG, "MediaProjectionCallback#onStop");
+            if (mVirtualDisplay != null) {
+                mVirtualDisplay.release();
+                mVirtualDisplay = null;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/PixelChecker.java b/tests/tests/view/src/android/view/cts/surfacevalidator/PixelChecker.java
new file mode 100644
index 0000000..76f0adc
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/PixelChecker.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+public interface PixelChecker {
+    boolean checkPixels(int blackishPixelCount, int width, int height);
+}
\ No newline at end of file
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/PixelCounter.rs b/tests/tests/view/src/android/view/cts/surfacevalidator/PixelCounter.rs
new file mode 100644
index 0000000..f58b9cb
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/PixelCounter.rs
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+#pragma version(1)
+#pragma rs java_package_name(android.view.cts.surfacevalidator)
+#pragma rs reduce(countBlackishPixels) accumulator(countBlackishPixelsAccum) combiner(countBlackishPixelsCombiner)
+
+uchar THRESHOLD;
+
+static void countBlackishPixelsAccum(int *accum, uchar4 pixel){
+    if (pixel.r < THRESHOLD
+            && pixel.g < THRESHOLD
+            && pixel.b < THRESHOLD) {
+        *accum += 1;
+    }
+}
+
+static void countBlackishPixelsCombiner(int *accum, const int *other){
+    *accum += *other;
+}
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java b/tests/tests/view/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java
new file mode 100644
index 0000000..5a30b77
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Trace;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.Type;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Surface;
+import android.view.cts.surfacevalidator.PixelChecker;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+public class SurfacePixelValidator {
+    private static final String TAG = "SurfacePixelValidator";
+
+    /**
+     * Observed that first few frames have errors with SurfaceView placement, so we skip for now.
+     * b/29603849 tracking that issue.
+     */
+    private static final int NUM_FIRST_FRAMES_SKIPPED = 8;
+
+    // If no channel is greater than this value, pixel will be considered 'blackish'.
+    private static final short PIXEL_CHANNEL_THRESHOLD = 4;
+
+    private static final int MAX_CAPTURED_FAILURES = 5;
+
+    private final int mWidth;
+    private final int mHeight;
+
+    private final HandlerThread mWorkerThread;
+    private final Handler mWorkerHandler;
+
+    private final PixelChecker mPixelChecker;
+
+    private final RenderScript mRS;
+
+    private final Allocation mInPixelsAllocation;
+    private final ScriptC_PixelCounter mScript;
+
+
+    private final Object mResultLock = new Object();
+    private int mResultSuccessFrames;
+    private int mResultFailureFrames;
+    private SparseArray<Bitmap> mFirstFailures = new SparseArray<>(MAX_CAPTURED_FAILURES);
+
+    private Runnable mConsumeRunnable = new Runnable() {
+        int numSkipped = 0;
+        @Override
+        public void run() {
+            Trace.beginSection("consume buffer");
+            mInPixelsAllocation.ioReceive();
+            Trace.endSection();
+
+            Trace.beginSection("compare and sum");
+            int blackishPixelCount = mScript.reduce_countBlackishPixels(mInPixelsAllocation).get();
+            Trace.endSection();
+
+            boolean success = mPixelChecker.checkPixels(blackishPixelCount, mWidth, mHeight);
+            synchronized (mResultLock) {
+                if (numSkipped < NUM_FIRST_FRAMES_SKIPPED) {
+                    numSkipped++;
+                } else {
+                    if (success) {
+                        mResultSuccessFrames++;
+                    } else {
+                        mResultFailureFrames++;
+                        int totalFramesSeen = mResultSuccessFrames + mResultFailureFrames;
+                        Log.d(TAG, "Failure (pixel count = " + blackishPixelCount
+                                + ") occurred on frame " + totalFramesSeen);
+
+                        if (mFirstFailures.size() < MAX_CAPTURED_FAILURES) {
+                            Log.d(TAG, "Capturing bitmap #" + mFirstFailures.size());
+                            // error, worth looking at...
+                            Bitmap capture = Bitmap.createBitmap(mWidth, mHeight,
+                                    Bitmap.Config.ARGB_8888);
+                            mInPixelsAllocation.copyTo(capture);
+                            mFirstFailures.put(totalFramesSeen, capture);
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    public SurfacePixelValidator(Context context, Point size, PixelChecker pixelChecker) {
+        mWidth = size.x;
+        mHeight = size.y;
+
+        mWorkerThread = new HandlerThread("SurfacePixelValidator");
+        mWorkerThread.start();
+        mWorkerHandler = new Handler(mWorkerThread.getLooper());
+
+        mPixelChecker = pixelChecker;
+
+        mRS = RenderScript.create(context);
+        mScript = new ScriptC_PixelCounter(mRS);
+
+        mInPixelsAllocation = createBufferQueueAllocation();
+        mScript.set_THRESHOLD(PIXEL_CHANNEL_THRESHOLD);
+
+        mInPixelsAllocation.setOnBufferAvailableListener(
+                allocation -> mWorkerHandler.post(mConsumeRunnable));
+    }
+
+    public Surface getSurface() {
+        return mInPixelsAllocation.getSurface();
+    }
+
+    private Allocation createBufferQueueAllocation() {
+        return Allocation.createAllocations(mRS, Type.createXY(mRS,
+                Element.RGBA_8888(mRS)
+                /*Element.U32(mRS)*/, mWidth, mHeight),
+                Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_INPUT,
+                1)[0];
+    }
+
+    /**
+     * Shuts down processing pipeline, and returns current pass/fail counts.
+     *
+     * Wait for pipeline to flush before calling this method. If not, frames that are still in
+     * flight may be lost.
+     */
+    public void finish(CapturedActivity.TestResult testResult) {
+        synchronized (mResultLock) {
+            // could in theory miss results still processing, but only if latency is extremely high.
+            // Caller should only call this
+            testResult.failFrames = mResultFailureFrames;
+            testResult.passFrames = mResultSuccessFrames;
+
+            for (int i = 0; i < mFirstFailures.size(); i++) {
+                testResult.failures.put(mFirstFailures.keyAt(i), mFirstFailures.valueAt(i));
+            }
+        }
+        mWorkerThread.quitSafely();
+    }
+}
diff --git a/tests/tests/view/src/android/view/cts/surfacevalidator/ViewFactory.java b/tests/tests/view/src/android/view/cts/surfacevalidator/ViewFactory.java
new file mode 100644
index 0000000..9ef2ef8
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/ViewFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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.view.cts.surfacevalidator;
+
+import android.content.Context;
+import android.view.View;
+
+public interface ViewFactory {
+    View createView(Context context);
+}