Tests for SurfaceView synchronization

bug:29242061

Capture moving/animating SurfaceViews via VirtualDisplay, and validate
its rendering content with RenderScript.

Change-Id: I84921d0ef8576ee5534f50a25088696538c1b562
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..ba1f3d2 100644
--- a/tests/tests/view/AndroidManifest.xml
+++ b/tests/tests/view/AndroidManifest.xml
@@ -239,6 +239,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"
+                  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..da453dd
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/SurfaceViewSyncTests.java
@@ -0,0 +1,300 @@
+/*
+ * 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.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.media.MediaPlayer;
+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.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 org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+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 = 500;
+
+    @Before
+    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)) {
+            assertTrue(acceptButton.click());
+        }
+    }
+
+    private CapturedActivity getActivity() {
+        return (CapturedActivity) mActivityRule.getActivity();
+    }
+
+    private MediaPlayer getMediaPlayer() {
+        return getActivity().getMediaPlayer();
+    }
+
+    @Rule
+    public ActivityTestRule mActivityRule = new ActivityTestRule<>(CapturedActivity.class);
+
+    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));
+    };
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Tests
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** Draws a moving 10x10 black rectangle, validates 100 pixels of black are seen each frame */
+    @Test
+    public void testSmallRect() {
+        CapturedActivity.TestResult result = getActivity().runTest(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 == 100));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    /**
+     * Verifies that a SurfaceView without a surface is entirely black, with pixel count being
+     * approximate to avoid rounding brittleness.
+     */
+    @Test
+    public void testEmptySurfaceView() {
+        CapturedActivity.TestResult result = getActivity().runTest(new AnimationTestCase(
+                sEmptySurfaceViewFactory,
+                new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP),
+                sTranslateAnimationFactory,
+                (blackishPixelCount, width, height) ->
+                        blackishPixelCount > 9000 && blackishPixelCount < 11000));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testSurfaceViewSmallScale() {
+        CapturedActivity.TestResult result = getActivity().runTest(new AnimationTestCase(
+                sGreenSurfaceViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sSmallScaleAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testSurfaceViewBigScale() {
+        CapturedActivity.TestResult result = getActivity().runTest(new AnimationTestCase(
+                sGreenSurfaceViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sBigScaleAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testVideoSurfaceViewTranslate() {
+        CapturedActivity.TestResult result = getActivity().runTest(new AnimationTestCase(
+                sVideoViewFactory,
+                new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP),
+                sTranslateAnimationFactory,
+                (blackishPixelCount, width, height) -> blackishPixelCount == 0));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testVideoSurfaceViewRotated() {
+        CapturedActivity.TestResult result = getActivity().runTest(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));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testVideoSurfaceViewEdgeCoverage() {
+        CapturedActivity.TestResult result = getActivity().runTest(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));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 0);
+    }
+
+    @Test
+    public void testVideoSurfaceViewCornerCoverage() {
+        CapturedActivity.TestResult result = getActivity().runTest(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));
+        assertTrue(result.passFrames > 100);
+        assertTrue(result.failFrames == 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..a198136
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/CapturedActivity.java
@@ -0,0 +1,197 @@
+/*
+ * 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.res.Configuration;
+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.view.Display;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import android.view.cts.R;
+
+public class CapturedActivity extends Activity {
+    public static class TestResult {
+        public int passFrames;
+        public int failFrames;
+    }
+
+    private static final String TAG = "CapturedActivity";
+    private static final long TIME_OUT_MS = 10000;
+    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();
+
+    private static final long START_CAPTURE_DELAY_MS = 1000;
+    private static final long END_CAPTURE_DELAY_MS = START_CAPTURE_DELAY_MS + 4000;
+    private static final long END_DELAY_MS = END_CAPTURE_DELAY_MS + 500;
+
+    private MediaPlayer mMediaPlayer;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private volatile boolean mOnWatch;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
+
+        mProjectionManager =
+                (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+
+        startActivityForResult(mProjectionManager.createScreenCaptureIntent(), PERMISSION_CODE);
+
+        mMediaPlayer = MediaPlayer.create(this, R.raw.colors_video);
+        mMediaPlayer.setLooping(true);
+
+        int uiMode = getResources().getConfiguration().uiMode;
+        mOnWatch = (uiMode & Configuration.UI_MODE_TYPE_WATCH) == Configuration.UI_MODE_TYPE_WATCH;
+    }
+
+    /**
+     * 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 (requestCode != PERMISSION_CODE) {
+            throw new IllegalStateException("Unknown request code: " + requestCode);
+        }
+        if (resultCode != RESULT_OK) {
+            throw new IllegalStateException("User denied screen sharing permission");
+        }
+        mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
+        mMediaProjection.registerCallback(new MediaProjectionCallback(), null);
+    }
+
+    public TestResult runTest(AnimationTestCase animationTestCase) {
+        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;
+        }
+
+        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) {
+            try {
+                mLock.wait(TIME_OUT_MS);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        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..55bc251
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/PixelCounter.rs
@@ -0,0 +1,36 @@
+/*
+ * 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)
+
+int WIDTH;
+uchar THRESHOLD;
+
+rs_allocation image;
+
+void countBlackishPixels(const int32_t *v_in, int *v_out){
+    int y = v_in[0];
+    v_out[0] = 0;
+
+    for(int i = 0 ; i < WIDTH; i++){
+        uchar4 pixel = rsGetElementAt_uchar4(image, i, y);
+        if (pixel.r < THRESHOLD
+                && pixel.g < THRESHOLD
+                && pixel.b < THRESHOLD) {
+            v_out[0]++;
+        }
+    }
+}
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..c9bff1d
--- /dev/null
+++ b/tests/tests/view/src/android/view/cts/surfacevalidator/SurfacePixelValidator.java
@@ -0,0 +1,183 @@
+/*
+ * 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.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.view.Surface;
+import android.view.cts.surfacevalidator.PixelChecker;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+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 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 Allocation mInRowsAllocation;
+    private final Allocation mOutRowsAllocation;
+    private final ScriptC_PixelCounter mScript;
+
+
+    private final Object mResultLock = new Object();
+    private int mResultSuccessFrames;
+    private int mResultFailureFrames;
+
+    private Runnable mConsumeRunnable = new Runnable() {
+        int numSkipped = 0;
+        @Override
+        public void run() {
+            Trace.beginSection("consume buffer");
+            mInPixelsAllocation.ioReceive();
+            mScript.set_image(mInPixelsAllocation);
+            Trace.endSection();
+
+            Trace.beginSection("compare");
+            mScript.forEach_countBlackishPixels(mInRowsAllocation, mOutRowsAllocation);
+            Trace.endSection();
+
+            Trace.beginSection("sum");
+            int blackishPixelCount = sum1DIntAllocation(mOutRowsAllocation, mHeight);
+            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);
+                    }
+                }
+            }
+        }
+    };
+
+    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();
+        mInRowsAllocation = createInputRowIndexAllocation();
+        mOutRowsAllocation = createOutputRowAllocation();
+        mScript.set_WIDTH(mWidth);
+        mScript.set_THRESHOLD(PIXEL_CHANNEL_THRESHOLD);
+
+        mInPixelsAllocation.setOnBufferAvailableListener(
+                allocation -> mWorkerHandler.post(mConsumeRunnable));
+    }
+
+    public Surface getSurface() {
+        return mInPixelsAllocation.getSurface();
+    }
+
+    static private int sum1DIntAllocation(Allocation array, int length) {
+        //Get the values returned from the function
+        int[] returnValue = new int[length];
+        array.copyTo(returnValue);
+        int sum = 0;
+        //If any row had any different pixels, then it fails
+        for (int i = 0; i < length; i++) {
+            sum += returnValue[i];
+        }
+        return sum;
+    }
+
+    /**
+     * Creates an allocation where the values in it are the indices of each row
+     */
+    private Allocation createInputRowIndexAllocation() {
+        //Create an array with the index of each row
+        int[] inputIndices = new int[mHeight];
+        for (int i = 0; i < mHeight; i++) {
+            inputIndices[i] = i;
+        }
+        //Create the allocation from that given array
+        Allocation inputAllocation = Allocation.createSized(mRS, Element.I32(mRS),
+                inputIndices.length, Allocation.USAGE_SCRIPT);
+        inputAllocation.copyFrom(inputIndices);
+        return inputAllocation;
+    }
+
+    private Allocation createOutputRowAllocation() {
+        return Allocation.createSized(mRS, Element.I32(mRS), mHeight, Allocation.USAGE_SCRIPT);
+    }
+
+    private Allocation createBufferQueueAllocation() {
+        return Allocation.createAllocations(mRS, Type.createXY(mRS,
+                Element.U8_4(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;
+        }
+        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);
+}