Jank test for full-screen activity orientation change.

Bug: 24142738

Change-Id: Id1a0d9fc78a71812f60d542f2bee91e3ff497ce6
diff --git a/tests/WindowAnimationJank/Android.mk b/tests/WindowAnimationJank/Android.mk
new file mode 100644
index 0000000..888ae64
--- /dev/null
+++ b/tests/WindowAnimationJank/Android.mk
@@ -0,0 +1,31 @@
+# Copyright (C) 2015 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := WindowAnimationJank
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/WindowAnimationJank/AndroidManifest.xml b/tests/WindowAnimationJank/AndroidManifest.xml
new file mode 100644
index 0000000..d7aef33
--- /dev/null
+++ b/tests/WindowAnimationJank/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ * Copyright (C) 2015 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.windowanimationjank">
+
+  <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
+
+  <application>
+      <uses-library android:name="android.test.runner"/>
+      <activity android:name="ElementLayoutActivity"
+                android:label="ElementLayoutActivity"
+                android:taskAffinity="android.windowanimationjank.ElementLayoutActivity" >
+          <intent-filter>
+              <action android:name="android.intent.action.MAIN" />
+              <category android:name="android.intent.category.LAUNCHER" />
+          </intent-filter>
+      </activity>
+  </application>
+
+    <!--  self-instrumenting test package. -->
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+                     android:targetPackage="android.windowanimationjank">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/WindowAnimationJank/res/layout/flowlayout.xml b/tests/WindowAnimationJank/res/layout/flowlayout.xml
new file mode 100644
index 0000000..f2b559b
--- /dev/null
+++ b/tests/WindowAnimationJank/res/layout/flowlayout.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2015 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.
+ -->
+<android.windowanimationjank.FlowLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/root_flow_layout"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent" />
\ No newline at end of file
diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/ElementLayoutActivity.java b/tests/WindowAnimationJank/src/android/windowanimationjank/ElementLayoutActivity.java
new file mode 100644
index 0000000..3b1fabc
--- /dev/null
+++ b/tests/WindowAnimationJank/src/android/windowanimationjank/ElementLayoutActivity.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 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.windowanimationjank;
+
+import java.util.Random;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.widget.Chronometer;
+import android.widget.RadioButton;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+/*
+ * Activity with arbitrary number of random UI elements, refresh itself constantly.
+ */
+public class ElementLayoutActivity extends Activity implements OnPreDrawListener {
+    public final static String NUM_ELEMENTS_KEY = "num_elements";
+
+    private final static int DEFAULT_NUM_ELEMENTS = 100;
+    private final static int BACKGROUND_COLOR = 0xfffff000;
+    private final static int INDICATOR_COLOR = 0xffff0000;
+
+    private FlowLayout mLayout;
+    // Use the constant seed in order to get predefined order of elements.
+    private Random mRandom = new Random(0);
+    // Blinker indicator for visual feedback that Activity is currently updating.
+    private TextView mIndicator;
+    private static float mIndicatorState;
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.flowlayout);
+
+        mLayout = (FlowLayout)findViewById(R.id.root_flow_layout);
+        mLayout.setBackgroundColor(BACKGROUND_COLOR);
+
+        mIndicator = new TextView(this);
+        mLayout.addView(mIndicator);
+        mIndicator.setText("***\n***");
+        mIndicator.setBackgroundColor(BACKGROUND_COLOR);
+        mIndicatorState = 0.0f;
+
+        // Need constantly invalidate view in order to get max redraw rate.
+        mLayout.getViewTreeObserver().addOnPreDrawListener(this);
+
+        // Read requested number of elements in layout.
+        int numElements = getIntent().getIntExtra(NUM_ELEMENTS_KEY, DEFAULT_NUM_ELEMENTS);
+
+        for (int i = 0; i < numElements; ++i) {
+            switch (mRandom.nextInt(5)) {
+            case 0:
+                createRadioButton();
+                break;
+            case 1:
+                createToggleButton();
+                break;
+            case 2:
+                createSwitch();
+                break;
+            case 3:
+                createTextView();
+                break;
+            case 4:
+                createChronometer();
+                break;
+            }
+        }
+
+        setContentView(mLayout);
+    }
+
+    private void createTextView() {
+        TextView textView = new TextView(this);
+        int lineCnt = mRandom.nextInt(4);
+        StringBuffer buffer = new StringBuffer();
+        for (int i = 0; i < lineCnt; ++i) {
+            if (i != 0) {
+                buffer.append("\n");
+            }
+            buffer.append("Line:" + mRandom.nextInt());
+        }
+        textView.setText(buffer);
+        mLayout.addView(textView);
+    }
+
+    private void createRadioButton() {
+        RadioButton button = new RadioButton(this);
+        button.setText("RadioButton:" + mRandom.nextInt());
+        mLayout.addView(button);
+    }
+
+    private void createToggleButton() {
+        ToggleButton button = new ToggleButton(this);
+        button.setChecked(mRandom.nextBoolean());
+        mLayout.addView(button);
+    }
+
+    private void createSwitch() {
+        Switch button = new Switch(this);
+        button.setChecked(mRandom.nextBoolean());
+        mLayout.addView(button);
+    }
+
+    private void createChronometer() {
+        Chronometer chronometer = new Chronometer(this);
+        chronometer.setBase(mRandom.nextLong());
+        mLayout.addView(chronometer);
+        chronometer.start();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    public boolean onPreDraw() {
+        // Interpolate indicator color
+        int background = 0xff000000;
+        for (int i = 0; i < 3; ++i) {
+            int shift = 8 * i;
+            int colorB = (BACKGROUND_COLOR >> shift) & 0xff;
+            int colorI = (INDICATOR_COLOR >> shift) & 0xff;
+            int color = (int)((float)colorB * (1.0f - mIndicatorState) +
+                    (float)colorI * mIndicatorState);
+            if (color > 255) {
+                color = 255;
+            }
+            background |= (color << shift);
+        }
+
+        mIndicator.setBackgroundColor(background);
+        mIndicatorState += (3 / 60.0f);  // around 3 times per second
+        mIndicatorState = mIndicatorState - (int)mIndicatorState;
+
+        mLayout.postInvalidate();
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/FlowLayout.java b/tests/WindowAnimationJank/src/android/windowanimationjank/FlowLayout.java
new file mode 100644
index 0000000..9a2b9cc
--- /dev/null
+++ b/tests/WindowAnimationJank/src/android/windowanimationjank/FlowLayout.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.windowanimationjank;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Custom layout that place all elements in flows with and automatically wraps them.
+ */
+public class FlowLayout extends ViewGroup {
+    private int mLineHeight;
+
+    public FlowLayout(Context context) {
+        super(context);
+    }
+
+    public FlowLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int width =
+                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() -getPaddingRight();
+        int height =
+                MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
+        final int count = getChildCount();
+
+        int x = getPaddingLeft();
+        int y = getPaddingTop();
+        int lineHeight = 0;
+
+        int childHeightMeasureSpec;
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+        } else {
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        }
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                        childHeightMeasureSpec);
+                final int childWidth = child.getMeasuredWidth();
+                lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
+
+                if (x + childWidth > width) {
+                    x = getPaddingLeft();
+                    y += lineHeight;
+                }
+
+                x += childWidth;
+            }
+        }
+        mLineHeight = lineHeight;
+
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+            height = y + lineHeight;
+        } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
+            if (y + lineHeight < height) {
+                height = y + lineHeight;
+            }
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        if (p instanceof LayoutParams) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int count = getChildCount();
+        final int width = r - l;
+        int x = getPaddingLeft();
+        int y = getPaddingTop();
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final int childWidth = child.getMeasuredWidth();
+                final int childHeight = child.getMeasuredHeight();
+                if (x + childWidth > width) {
+                    x = getPaddingLeft();
+                    y += mLineHeight;
+                }
+                child.layout(x, y, x + childWidth, y + childHeight);
+                x += childWidth;
+            }
+        }
+    }
+}
diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java b/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java
new file mode 100644
index 0000000..1fb502a
--- /dev/null
+++ b/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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.windowanimationjank;
+
+import android.os.Bundle;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.GfxMonitor;
+
+/**
+ * Detect janks during screen rotation for full-screen activity. Periodically change
+ * orientation from left to right and track ElementLayoutActivity rendering performance
+ * via GfxMonitor.
+ */
+public class FullscreenRotationTest extends WindowAnimationJankTestBase {
+    private final static int STEP_CNT = 3;
+
+    @Override
+    public void beforeTest() throws Exception {
+        getUiDevice().setOrientationLeft();
+        Utils.startElementLayout(getInstrumentation(), 100);
+        super.beforeTest();
+    }
+
+    @Override
+    public void afterTest(Bundle metrics) {
+        Utils.rotateDevice(getInstrumentation(), Utils.ROTATION_MODE_NATURAL);
+        super.afterTest(metrics);
+    }
+
+    @JankTest(expectedFrames=100, defaultIterationCount=2)
+    @GfxMonitor(processName=Utils.PACKAGE)
+    public void testRotation() throws Exception {
+        for (int i = 0; i < STEP_CNT; ++i) {
+            Utils.rotateDevice(getInstrumentation(),
+                    Utils.getDeviceRotation(getInstrumentation()) == Utils.ROTATION_MODE_LEFT ?
+                    Utils.ROTATION_MODE_RIGHT : Utils.ROTATION_MODE_LEFT);
+        }
+    }
+}
diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/Utils.java b/tests/WindowAnimationJank/src/android/windowanimationjank/Utils.java
new file mode 100644
index 0000000..2531464
--- /dev/null
+++ b/tests/WindowAnimationJank/src/android/windowanimationjank/Utils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2015 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.windowanimationjank;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+/**
+ * Set of helpers to manipulate test activities.
+ */
+public class Utils {
+    protected final static String PACKAGE = "android.windowanimationjank";
+    protected final static String ELEMENT_LAYOUT_ACTIVITY = "ElementLayoutActivity";
+    protected final static String ELEMENT_LAYOUT_CLASS = PACKAGE + "." + ELEMENT_LAYOUT_ACTIVITY;
+    protected final static long WAIT_FOR_ACTIVITY_TIMEOUT = 10000;
+    private static final BySelector ROOT_ELEMENT_LAYOUT = By.res(PACKAGE, "root_flow_layout");
+
+    private final static long ROTATION_ANIMATION_TIME_FULL_SCREEN_MS = 1000;
+
+    protected final static int ROTATION_MODE_NATURAL = 0;
+    protected final static int ROTATION_MODE_LEFT = 1;
+    protected final static int ROTATION_MODE_RIGHT = 2;
+
+    private static UiObject2 waitForActivity(Instrumentation instrumentation, BySelector selector) {
+        UiDevice device = UiDevice.getInstance(instrumentation);
+        UiObject2 window = device.wait(Until.findObject(selector), WAIT_FOR_ACTIVITY_TIMEOUT);
+        if (window == null) {
+            throw new RuntimeException(selector.toString() + " has not been started.");
+        }
+
+        // Get root object.
+        while (window.getParent() != null) {
+            window = window.getParent();
+        }
+        return window;
+    }
+
+    public static UiObject2 waitForElementLayout(Instrumentation instrumentation) {
+        return waitForActivity(instrumentation, ROOT_ELEMENT_LAYOUT);
+    }
+
+    /**
+     * Start and return activity with requested number of random elements.
+     */
+    public static UiObject2 startElementLayout(Instrumentation instrumentation, int numElements) {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setComponent(new ComponentName(PACKAGE, ELEMENT_LAYOUT_CLASS));
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(ElementLayoutActivity.NUM_ELEMENTS_KEY, numElements);
+        instrumentation.getTargetContext().startActivity(intent);
+        return waitForElementLayout(instrumentation);
+    }
+
+    public static int getDeviceRotation(Instrumentation instrumentation) {
+        try {
+            UiDevice device = UiDevice.getInstance(instrumentation);
+            switch (device.getDisplayRotation()) {
+            case UiAutomation.ROTATION_FREEZE_90:
+                return ROTATION_MODE_LEFT;
+            case UiAutomation.ROTATION_FREEZE_270:
+                return ROTATION_MODE_RIGHT;
+            case UiAutomation.ROTATION_FREEZE_0:
+            case UiAutomation.ROTATION_FREEZE_180:
+                return ROTATION_MODE_NATURAL;
+            }
+        } catch(Exception e) {
+            throw new RuntimeException();
+        }
+        throw new RuntimeException("Unsupported device rotation.");
+    }
+
+    public static void rotateDevice(Instrumentation instrumentation, int rotationMode) {
+        try {
+            UiDevice device = UiDevice.getInstance(instrumentation);
+            long startTime = System.currentTimeMillis();
+            switch (rotationMode) {
+            case ROTATION_MODE_NATURAL:
+                device.setOrientationNatural();
+                break;
+            case ROTATION_MODE_LEFT:
+                device.setOrientationLeft();
+                break;
+            case ROTATION_MODE_RIGHT:
+                device.setOrientationRight();
+                break;
+            default:
+                throw new RuntimeException("Unsupported rotation mode: " + rotationMode);
+            }
+
+            long toSleep = ROTATION_ANIMATION_TIME_FULL_SCREEN_MS -
+                    (System.currentTimeMillis() - startTime);
+            if (toSleep > 0) {
+                SystemClock.sleep(toSleep);
+            }
+        } catch(Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java b/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java
new file mode 100644
index 0000000..bf739fa
--- /dev/null
+++ b/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.windowanimationjank;
+
+import java.io.IOException;
+import java.util.StringTokenizer;
+
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.UiDevice;
+
+/**
+ * This adds additional system level jank monitor and its result is merged with primary monitor
+ * used in test.
+ */
+public abstract class WindowAnimationJankTestBase extends JankTestBase {
+    private static final String TAG = "WindowAnimationJankTestBase";
+
+    protected WindowAnimationJankTestBase() {
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // fix device orientation
+        getUiDevice().setOrientationNatural();
+
+        // Start from the home screen
+        getUiDevice().pressHome();
+        getUiDevice().waitForIdle();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        getUiDevice().unfreezeRotation();
+        super.tearDown();
+    }
+
+    protected UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+}