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);
+}