Home | History | Annotate | Download | only in cts
      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(mActivity.getCaptureDurationMs());
    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