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