1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.view.cts; 17 18 import static org.junit.Assert.assertTrue; 19 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.animation.ValueAnimator; 23 import android.annotation.SuppressLint; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Paint; 28 import android.media.MediaPlayer; 29 import android.os.Environment; 30 import android.support.test.filters.LargeTest; 31 import android.support.test.rule.ActivityTestRule; 32 import android.support.test.runner.AndroidJUnit4; 33 import android.support.test.uiautomator.UiObjectNotFoundException; 34 import android.util.Log; 35 import android.util.SparseArray; 36 import android.view.Gravity; 37 import android.view.SurfaceHolder; 38 import android.view.SurfaceView; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.animation.LinearInterpolator; 42 import android.view.cts.surfacevalidator.AnimationFactory; 43 import android.view.cts.surfacevalidator.AnimationTestCase; 44 import android.view.cts.surfacevalidator.CapturedActivity; 45 import android.view.cts.surfacevalidator.ViewFactory; 46 import android.widget.FrameLayout; 47 48 import org.junit.After; 49 import org.junit.Before; 50 import org.junit.Rule; 51 import org.junit.Test; 52 import org.junit.rules.TestName; 53 import org.junit.runner.RunWith; 54 55 import java.io.File; 56 import java.io.FileOutputStream; 57 import java.io.IOException; 58 import java.util.concurrent.TimeUnit; 59 60 @RunWith(AndroidJUnit4.class) 61 @LargeTest 62 @SuppressLint("RtlHardcoded") 63 public class SurfaceViewSyncTest { 64 private static final String TAG = "SurfaceViewSyncTests"; 65 66 @Rule 67 public ActivityTestRule<CapturedActivity> mActivityRule = 68 new ActivityTestRule<>(CapturedActivity.class); 69 70 @Rule 71 public TestName mName = new TestName(); 72 73 private CapturedActivity mActivity; 74 private MediaPlayer mMediaPlayer; 75 76 @Before 77 public void setup() { 78 mActivity = mActivityRule.getActivity(); 79 mMediaPlayer = mActivity.getMediaPlayer(); 80 } 81 82 /** 83 * Want to be especially sure we don't leave up the permission dialog, so try and dismiss 84 * after test. 85 */ 86 @After 87 public void tearDown() throws UiObjectNotFoundException { 88 mActivity.dismissPermissionDialog(); 89 } 90 91 private static ValueAnimator makeInfinite(ValueAnimator a) { 92 a.setRepeatMode(ObjectAnimator.REVERSE); 93 a.setRepeatCount(ObjectAnimator.INFINITE); 94 a.setDuration(200); 95 a.setInterpolator(new LinearInterpolator()); 96 return a; 97 } 98 99 /////////////////////////////////////////////////////////////////////////// 100 // ViewFactories 101 /////////////////////////////////////////////////////////////////////////// 102 103 private ViewFactory sEmptySurfaceViewFactory = context -> { 104 SurfaceView surfaceView = new SurfaceView(context); 105 106 // prevent transparent region optimization, which is invalid for a SurfaceView moving around 107 surfaceView.setWillNotDraw(false); 108 109 return surfaceView; 110 }; 111 112 private ViewFactory sGreenSurfaceViewFactory = context -> { 113 SurfaceView surfaceView = new SurfaceView(context); 114 115 // prevent transparent region optimization, which is invalid for a SurfaceView moving around 116 surfaceView.setWillNotDraw(false); 117 118 surfaceView.getHolder().setFixedSize(640, 480); 119 surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 120 @Override 121 public void surfaceCreated(SurfaceHolder holder) {} 122 123 @Override 124 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 125 Canvas canvas = holder.lockCanvas(); 126 canvas.drawColor(Color.GREEN); 127 holder.unlockCanvasAndPost(canvas); 128 } 129 130 @Override 131 public void surfaceDestroyed(SurfaceHolder holder) {} 132 }); 133 return surfaceView; 134 }; 135 136 private ViewFactory sVideoViewFactory = context -> { 137 SurfaceView surfaceView = new SurfaceView(context); 138 139 // prevent transparent region optimization, which is invalid for a SurfaceView moving around 140 surfaceView.setWillNotDraw(false); 141 142 surfaceView.getHolder().setFixedSize(640, 480); 143 surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 144 @Override 145 public void surfaceCreated(SurfaceHolder holder) { 146 mMediaPlayer.setSurface(holder.getSurface()); 147 mMediaPlayer.start(); 148 } 149 150 @Override 151 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} 152 153 @Override 154 public void surfaceDestroyed(SurfaceHolder holder) { 155 mMediaPlayer.pause(); 156 mMediaPlayer.setSurface(null); 157 } 158 }); 159 return surfaceView; 160 }; 161 162 /////////////////////////////////////////////////////////////////////////// 163 // AnimationFactories 164 /////////////////////////////////////////////////////////////////////////// 165 166 private AnimationFactory sSmallScaleAnimationFactory = view -> { 167 view.setPivotX(0); 168 view.setPivotY(0); 169 PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.SCALE_X, 0.01f, 1f); 170 PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.01f, 1f); 171 return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY)); 172 }; 173 174 private AnimationFactory sBigScaleAnimationFactory = view -> { 175 view.setTranslationX(10); 176 view.setTranslationY(10); 177 view.setPivotX(0); 178 view.setPivotY(0); 179 PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f, 3f); 180 PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f, 3f); 181 return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY)); 182 }; 183 184 private AnimationFactory sTranslateAnimationFactory = view -> { 185 PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f); 186 PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f); 187 return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY)); 188 }; 189 190 /////////////////////////////////////////////////////////////////////////// 191 // Bad frame capture 192 /////////////////////////////////////////////////////////////////////////// 193 194 private void saveFailureCaptures(SparseArray<Bitmap> failFrames) { 195 if (failFrames.size() == 0) return; 196 197 String directoryName = Environment.getExternalStorageDirectory() 198 + "/" + getClass().getSimpleName() 199 + "/" + mName.getMethodName(); 200 File testDirectory = new File(directoryName); 201 if (testDirectory.exists()) { 202 String[] children = testDirectory.list(); 203 if (children == null) { 204 return; 205 } 206 for (String file : children) { 207 new File(testDirectory, file).delete(); 208 } 209 } else { 210 testDirectory.mkdirs(); 211 } 212 213 for (int i = 0; i < failFrames.size(); i++) { 214 int frameNr = failFrames.keyAt(i); 215 Bitmap bitmap = failFrames.valueAt(i); 216 217 String bitmapName = "frame_" + frameNr + ".png"; 218 Log.d(TAG, "Saving file : " + bitmapName + " in directory : " + directoryName); 219 220 File file = new File(directoryName, bitmapName); 221 try (FileOutputStream fileStream = new FileOutputStream(file)) { 222 bitmap.compress(Bitmap.CompressFormat.PNG, 85, fileStream); 223 fileStream.flush(); 224 } catch (IOException e) { 225 e.printStackTrace(); 226 } 227 } 228 } 229 230 /////////////////////////////////////////////////////////////////////////// 231 // Tests 232 /////////////////////////////////////////////////////////////////////////// 233 234 private void verifyTest(AnimationTestCase testCase) throws Throwable { 235 CapturedActivity.TestResult result = mActivity.runTest(testCase); 236 saveFailureCaptures(result.failures); 237 238 float failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames); 239 assertTrue("Error: " + failRatio + " fail ratio - extremely high, is activity obstructed?", 240 failRatio < 0.95f); 241 assertTrue("Error: " + result.failFrames 242 + " incorrect frames observed - incorrect positioning", 243 result.failFrames == 0); 244 float framesPerSecond = 1.0f * result.passFrames 245 / TimeUnit.MILLISECONDS.toSeconds(CapturedActivity.CAPTURE_DURATION_MS); 246 assertTrue("Error, only " + result.passFrames 247 + " frames observed, virtual display only capturing at " 248 + framesPerSecond + " frames per second", 249 result.passFrames > 100); 250 } 251 252 /** Draws a moving 10x10 black rectangle, validates 100 pixels of black are seen each frame */ 253 @Test 254 public void testSmallRect() throws Throwable { 255 verifyTest(new AnimationTestCase( 256 context -> new View(context) { 257 // draw a single pixel 258 final Paint sBlackPaint = new Paint(); 259 @Override 260 protected void onDraw(Canvas canvas) { 261 canvas.drawRect(0, 0, 10, 10, sBlackPaint); 262 } 263 264 @SuppressWarnings("unused") 265 void setOffset(int offset) { 266 // Note: offset by integer values, to ensure no rounding 267 // is done in rendering layer, as that may be brittle 268 setTranslationX(offset); 269 setTranslationY(offset); 270 } 271 }, 272 new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP), 273 view -> makeInfinite(ObjectAnimator.ofInt(view, "offset", 10, 30)), 274 (blackishPixelCount, width, height) -> 275 blackishPixelCount >= 90 && blackishPixelCount <= 110)); 276 } 277 278 /** 279 * Verifies that a SurfaceView without a surface is entirely black, with pixel count being 280 * approximate to avoid rounding brittleness. 281 */ 282 @Test 283 public void testEmptySurfaceView() throws Throwable { 284 verifyTest(new AnimationTestCase( 285 sEmptySurfaceViewFactory, 286 new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP), 287 sTranslateAnimationFactory, 288 (blackishPixelCount, width, height) -> 289 blackishPixelCount > 9000 && blackishPixelCount < 11000)); 290 } 291 292 @Test 293 public void testSurfaceViewSmallScale() throws Throwable { 294 verifyTest(new AnimationTestCase( 295 sGreenSurfaceViewFactory, 296 new FrameLayout.LayoutParams(320, 240, Gravity.LEFT | Gravity.TOP), 297 sSmallScaleAnimationFactory, 298 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 299 } 300 301 @Test 302 public void testSurfaceViewBigScale() throws Throwable { 303 verifyTest(new AnimationTestCase( 304 sGreenSurfaceViewFactory, 305 new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP), 306 sBigScaleAnimationFactory, 307 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 308 } 309 310 @Test 311 public void testVideoSurfaceViewTranslate() throws Throwable { 312 verifyTest(new AnimationTestCase( 313 sVideoViewFactory, 314 new FrameLayout.LayoutParams(640, 480, Gravity.LEFT | Gravity.TOP), 315 sTranslateAnimationFactory, 316 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 317 } 318 319 @Test 320 public void testVideoSurfaceViewRotated() throws Throwable { 321 verifyTest(new AnimationTestCase( 322 sVideoViewFactory, 323 new FrameLayout.LayoutParams(100, 100, Gravity.LEFT | Gravity.TOP), 324 view -> makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, 325 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 10f, 30f), 326 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 10f, 30f), 327 PropertyValuesHolder.ofFloat(View.ROTATION, 45f, 45f))), 328 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 329 } 330 331 @Test 332 public void testVideoSurfaceViewEdgeCoverage() throws Throwable { 333 verifyTest(new AnimationTestCase( 334 sVideoViewFactory, 335 new FrameLayout.LayoutParams(640, 480, Gravity.CENTER), 336 view -> { 337 ViewGroup parent = (ViewGroup) view.getParent(); 338 final int x = parent.getWidth() / 2; 339 final int y = parent.getHeight() / 2; 340 341 // Animate from left, to top, to right, to bottom 342 return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, 343 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, -x, 0, x, 0, -x), 344 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0, -y, 0, y, 0))); 345 }, 346 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 347 } 348 349 @Test 350 public void testVideoSurfaceViewCornerCoverage() throws Throwable { 351 verifyTest(new AnimationTestCase( 352 sVideoViewFactory, 353 new FrameLayout.LayoutParams(640, 480, Gravity.CENTER), 354 view -> { 355 ViewGroup parent = (ViewGroup) view.getParent(); 356 final int x = parent.getWidth() / 2; 357 final int y = parent.getHeight() / 2; 358 359 // Animate from top left, to top right, to bottom right, to bottom left 360 return makeInfinite(ObjectAnimator.ofPropertyValuesHolder(view, 361 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, -x, x, x, -x, -x), 362 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -y, -y, y, y, -y))); 363 }, 364 (blackishPixelCount, width, height) -> blackishPixelCount == 0)); 365 } 366 } 367