Merge changes I8cad9f50,I9406074e,I0ac64d8f,I01ab39a9,I2cb85c7e, ...

* changes:
  CTS Verifier test for Display.getSupportedModes().
  Test logic for Display.getMode().
  Empty CTS Verifier test for display modes.
  Don't recreate activity on display disconnected.
  Logging for CTS Verifier Test for HDR Capabilities
  Loading spinner for the async test.
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index af6dc5b..c528a3d6 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -57,6 +57,7 @@
 LOCAL_JAVA_LIBRARIES += android.test.base.stubs
 LOCAL_JAVA_LIBRARIES += android.test.mock.stubs
 LOCAL_JAVA_LIBRARIES += voip-common
+LOCAL_JAVA_LIBRARIES += truth-prebuilt
 
 LOCAL_PACKAGE_NAME := CtsVerifier
 LOCAL_PRIVATE_PLATFORM_APIS := true
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index cf0fa48..de7c8d6 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -3188,7 +3188,8 @@
         </activity>
 
         <activity android:name=".tv.display.DisplayHdrCapabilitiesTestActivity"
-                  android:label="@string/tv_hdr_capabilities_test">
+                  android:label="@string/tv_hdr_capabilities_test"
+                  android:configChanges="orientation|screenSize|density|smallestScreenSize|screenLayout">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
@@ -3197,6 +3198,18 @@
             <meta-data android:name="test_required_features"
                        android:value="android.software.leanback" />
         </activity>
+        <activity android:name=".tv.display.DisplayModesTestActivity"
+                  android:label="@string/tv_display_modes_test"
+                  android:configChanges="orientation|screenSize|density|smallestScreenSize|screenLayout">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_tv" />
+            <meta-data android:name="test_required_features"
+                       android:value="android.software.leanback" />
+        </activity>
+
 
         <activity android:name=".screenpinning.ScreenPinningTestActivity"
             android:label="@string/screen_pinning_test">
diff --git a/apps/CtsVerifier/res/layout/tv_item.xml b/apps/CtsVerifier/res/layout/tv_item.xml
index e7311f9..e2756ec 100644
--- a/apps/CtsVerifier/res/layout/tv_item.xml
+++ b/apps/CtsVerifier/res/layout/tv_item.xml
@@ -50,4 +50,12 @@
         android:layout_toRightOf="@id/status"
         android:visibility="gone" />
 
+    <ProgressBar
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/loadingSpinner"
+        android:layout_centerInParent="true"
+        android:layout_alignParentBottom="true"
+        android:visibility="invisible"/>
+
 </RelativeLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 7068605..14ce3f5 100755
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -4366,6 +4366,26 @@
         reconnect the display.
     </string>
 
+    <!-- Display Modes Test -->
+    <string name="tv_display_modes_test">Display Modes Test</string>
+    <string name="tv_display_modes_test_info">This test checks if Display.getSupportedModes()
+        and Display.getMode() are correctly reporting the supported screen modes.
+    </string>
+    <string name="tv_display_modes_disconnect_display">
+        Press the "%1$s" button and disconnect the display within %2$d seconds. Wait at least %3$d
+        seconds and then reconnect the display.
+    </string>
+    <string name="tv_display_modes_test_step_no_display">No Display</string>
+    <string name="tv_display_modes_test_step_1080p">1080p Display</string>
+    <string name="tv_display_modes_test_step_2160p">2160p Display</string>
+    <string name="tv_display_modes_start_test_button">Start Test</string>
+    <string name="tv_display_modes_connect_2160p_display">
+        Connect a 2160p display and press the "%s" button, below.
+    </string>
+    <string name="tv_display_modes_connect_1080p_display">
+        Connect a 1080p display and press the "%s" button, below.
+    </string>
+
     <string name="overlay_view_text">Overlay View Dummy Text</string>
     <string name="custom_rating">Example of input app specific custom rating.</string>
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/AsyncTestStep.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/AsyncTestStep.java
index e3559e0..37c33a3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/AsyncTestStep.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/AsyncTestStep.java
@@ -16,11 +16,14 @@
 
 package com.android.cts.verifier.tv.display;
 
+import android.view.View;
+
+import com.android.cts.verifier.R;
 import com.android.cts.verifier.tv.TvAppVerifierActivity;
 
 /**
  *  Encapsulates the logic of an asynchronous test step, which displays a human instructions and a
- *  button to start the test. For synchronous steps see {@link TestStep}.
+ *  button to start the test. For synchronous steps see {@link SyncTestStep}.
  */
 public abstract class AsyncTestStep extends TestStepBase {
 
@@ -29,8 +32,7 @@
     }
 
     /**
-     * Runs the test logic, when finished sets the test status by calling
-     * {@link AsyncTestStep#doneWithPassingState(boolean)}.
+     * Runs the test logic, when finished calls {@link AsyncTestStep#done()}.
      */
     public abstract void runTestAsync();
 
@@ -38,6 +40,23 @@
     protected void onButtonClickRunTest() {
         // Disable the button, so the user can't run it twice.
         disableButton();
+        showLoadingSpinner();
         runTestAsync();
     }
+
+    @Override
+    protected void done() {
+        hideLoadingSpinner();
+        super.done();
+    }
+
+    private void showLoadingSpinner() {
+        View spinner = mViewItem.findViewById(R.id.loadingSpinner);
+        spinner.setVisibility(View.VISIBLE);
+    }
+
+    private void hideLoadingSpinner() {
+        View spinner = mViewItem.findViewById(R.id.loadingSpinner);
+        spinner.setVisibility(View.INVISIBLE);
+    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
index d38d127..7123be7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayHdrCapabilitiesTestActivity.java
@@ -16,12 +16,16 @@
 
 package com.android.cts.verifier.tv.display;
 
-import android.content.Context;
 import android.view.Display;
 
+import androidx.annotation.StringRes;
+
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.tv.TvAppVerifierActivity;
 
+import com.google.common.base.Throwables;
+import com.google.common.collect.Range;
+
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -38,13 +42,6 @@
  * Display is connected and 3. no display is connected.
  */
 public class DisplayHdrCapabilitiesTestActivity extends TvAppVerifierActivity {
-    private static final @Display.HdrCapabilities.HdrType
-    int[] EXPECTED_SUPPORTED_HDR_TYPES_SORTED = {
-            Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION,
-            Display.HdrCapabilities.HDR_TYPE_HDR10,
-            Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS,
-            Display.HdrCapabilities.HDR_TYPE_HLG
-    };
     private static final float MAX_EXPECTED_LUMINANCE = 10_000f;
     private static final int DISPLAY_DISCONNECT_WAIT_TIME_SECONDS = 5;
 
@@ -57,6 +54,11 @@
     }
 
     @Override
+    public String getTestDetails() {
+        return mTestSequence.getFailureDetails();
+    }
+
+    @Override
     protected void createTestItems() {
         List<TestStepBase> testSteps = new ArrayList<>();
         testSteps.add(new NonHdrDisplayTestStep(this));
@@ -67,11 +69,6 @@
         mTestSequence.init();
     }
 
-    private static boolean hasNoHdrSupportedTypes(Display display) {
-        return display.getHdrCapabilities() == null
-                || display.getHdrCapabilities().getSupportedHdrTypes().length == 0;
-    }
-
     private static class NonHdrDisplayTestStep extends SyncTestStep {
 
         public NonHdrDisplayTestStep(TvAppVerifierActivity context) {
@@ -85,14 +82,26 @@
         }
 
         @Override
-        protected int getButtonStringId() {
+        protected String getStepName() {
+            return "Non HDR Display";
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
             return R.string.tv_start_test;
         }
 
         @Override
-        public boolean runTest() {
+        public void runTest() {
             Display display = mContext.getWindowManager().getDefaultDisplay();
-            return !display.isHdr() && hasNoHdrSupportedTypes(display);
+            getAsserter()
+                    .withMessage("Display.isHdr()")
+                    .that(display.isHdr())
+                    .isFalse();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities()")
+                    .that(display.getHdrCapabilities().getSupportedHdrTypes())
+                    .isEmpty();
         }
     }
 
@@ -109,44 +118,55 @@
         }
 
         @Override
-        protected int getButtonStringId() {
+        protected String getStepName() {
+            return "HDR Display";
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
             return R.string.tv_start_test;
         }
 
         @Override
-        public boolean runTest() {
+        public void runTest() {
             Display display = mContext.getWindowManager().getDefaultDisplay();
-            return display.isHdr()
-                    && hasExpectedHdrSupportedTypes(display)
-                    && hasSaneLuminanceValues(display);
-        }
 
-        private static boolean hasExpectedHdrSupportedTypes(Display display) {
-            Display.HdrCapabilities actualHdrCapabilities = display.getHdrCapabilities();
-            int[] actualSupportedHdrTypes = actualHdrCapabilities.getSupportedHdrTypes();
-            return Arrays.equals(EXPECTED_SUPPORTED_HDR_TYPES_SORTED, actualSupportedHdrTypes);
-        }
+            getAsserter()
+                    .withMessage("Display.isHdr()")
+                    .that(display.isHdr())
+                    .isTrue();
 
-        private static boolean hasSaneLuminanceValues(Display display) {
             Display.HdrCapabilities hdrCapabilities = display.getHdrCapabilities();
 
+            int[] supportedHdrTypes = hdrCapabilities.getSupportedHdrTypes();
+            Arrays.sort(supportedHdrTypes);
+
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getSupportedTypes()")
+                    .that(supportedHdrTypes)
+                    .isEqualTo(new int[]{
+                        Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION,
+                        Display.HdrCapabilities.HDR_TYPE_HDR10,
+                        Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS,
+                        Display.HdrCapabilities.HDR_TYPE_HLG
+                    });
+
             float maxLuminance = hdrCapabilities.getDesiredMaxLuminance();
-            float maxAvgLuminance = hdrCapabilities.getDesiredMaxAverageLuminance();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMaxLuminance()")
+                    .that(maxLuminance)
+                    .isIn(Range.openClosed(0f, MAX_EXPECTED_LUMINANCE));
+
             float minLuminance = hdrCapabilities.getDesiredMinLuminance();
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMinLuminance()")
+                    .that(minLuminance)
+                    .isIn(Range.closedOpen(0f, MAX_EXPECTED_LUMINANCE));
 
-            if(!(0f < maxLuminance && maxLuminance <= MAX_EXPECTED_LUMINANCE)) {
-                return false;
-            }
-
-            if(!(0f < maxAvgLuminance && maxAvgLuminance <= MAX_EXPECTED_LUMINANCE)) {
-                return false;
-            }
-
-            if (!(minLuminance < maxAvgLuminance && maxAvgLuminance <= maxLuminance)) {
-                return false;
-            }
-
-            return true;
+            getAsserter()
+                    .withMessage("Display.getHdrCapabilities().getDesiredMaxAverageLuminance()")
+                    .that(hdrCapabilities.getDesiredMaxAverageLuminance())
+                    .isIn(Range.openClosed(minLuminance, maxLuminance));
         }
     }
 
@@ -164,7 +184,12 @@
         }
 
         @Override
-        protected int getButtonStringId() {
+        protected String getStepName() {
+            return "No Display";
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
             return R.string.tv_start_test;
         }
 
@@ -172,8 +197,15 @@
         public void runTestAsync() {
             // Wait for the user to disconnect the display.
             mContext.getPostTarget().postDelayed(() -> {
-                Display display = mContext.getWindowManager().getDefaultDisplay();
-                doneWithPassingState(!display.isHdr() && hasNoHdrSupportedTypes(display));
+                try {
+                    // Verify the display APIs do not crash when the display is disconnected
+                    Display display = mContext.getWindowManager().getDefaultDisplay();
+                    display.isHdr();
+                    display.getHdrCapabilities();
+                } catch (Exception e) {
+                    getAsserter().fail(Throwables.getStackTraceAsString(e));
+                }
+                done();
             }, Duration.ofSeconds(DISPLAY_DISCONNECT_WAIT_TIME_SECONDS).toMillis());
         }
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java
new file mode 100644
index 0000000..c9b4de1
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/DisplayModesTestActivity.java
@@ -0,0 +1,287 @@
+/*
+ * 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 com.android.cts.verifier.tv.display;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.view.Display;
+
+import androidx.annotation.StringRes;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.tv.TvAppVerifierActivity;
+
+import com.google.common.base.Throwables;
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Test for verifying that the platform correctly reports display resolution and refresh rate. More
+ * specifically Display.getMode() and Display.getSupportedModes() APIs are tested against reference
+ * displays.
+ */
+public class DisplayModesTestActivity extends TvAppVerifierActivity {
+    private static final int DISPLAY_DISCONNECT_WAIT_TIME_SECONDS = 5;
+    private static final float REFRESH_RATE_PRECISION = 0.01f;
+
+    private static final Subject.Factory<ModeSubject, Display.Mode> MODE_SUBJECT_FACTORY =
+            (failureMetadata, mode) -> new ModeSubject(failureMetadata, mode);
+
+    private static final Correspondence<Display.Mode, Mode> MODE_CORRESPONDENCE =
+            new Correspondence<Display.Mode, Mode>() {
+                @Override
+                public boolean compare(Display.Mode displayMode, Mode mode) {
+                    return mode.isEquivalent(displayMode, REFRESH_RATE_PRECISION);
+                }
+
+                @Override
+                public String toString() {
+                    return "is equivalent to";
+                }
+            };
+
+    private TestSequence mTestSequence;
+    private DisplayManager mDisplayManager;
+
+    @Override
+    protected void setInfoResources() {
+        setInfoResources(R.string.tv_display_modes_test, R.string.tv_display_modes_test_info, -1);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mDisplayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
+    }
+
+    @Override
+    protected void createTestItems() {
+        List<TestStepBase> testSteps = new ArrayList<>();
+        testSteps.add(new NoDisplayTestStep(this));
+        testSteps.add(new Display2160pTestStep(this));
+        testSteps.add(new Display1080pTestStep(this));
+        mTestSequence = new TestSequence(this, testSteps);
+        mTestSequence.init();
+    }
+
+    @Override
+    public String getTestDetails() {
+        return mTestSequence.getFailureDetails();
+    }
+
+    private class NoDisplayTestStep extends AsyncTestStep {
+        public NoDisplayTestStep(TvAppVerifierActivity context) {
+            super(context);
+        }
+
+        @Override
+        protected String getStepName() {
+            return mContext.getString(R.string.tv_display_modes_test_step_no_display);
+        }
+
+        @Override
+        protected String getInstructionText() {
+            return mContext.getString(
+                    R.string.tv_display_modes_disconnect_display,
+                    mContext.getString(getButtonStringId()),
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS,
+                    DISPLAY_DISCONNECT_WAIT_TIME_SECONDS + 1);
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTestAsync() {
+            mContext.getPostTarget()
+                    .postDelayed(
+                            () -> {
+                                try {
+                                    // Verify the display APIs do not crash when the display is
+                                    // disconnected
+                                    Display display =
+                                            mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+                                    display.getMode();
+                                    display.getSupportedModes();
+                                } catch (Exception e) {
+                                    getAsserter().fail(Throwables.getStackTraceAsString(e));
+                                }
+                                done();
+                            },
+                            Duration.ofSeconds(DISPLAY_DISCONNECT_WAIT_TIME_SECONDS).toMillis());
+        }
+    }
+
+    private class Display2160pTestStep extends SyncTestStep {
+        public Display2160pTestStep(TvAppVerifierActivity context) {
+            super(context);
+        }
+
+        @Override
+        protected String getStepName() {
+            return mContext.getString(R.string.tv_display_modes_test_step_2160p);
+        }
+
+        @Override
+        protected String getInstructionText() {
+            return mContext.getString(
+                    R.string.tv_display_modes_connect_2160p_display,
+                    mContext.getString(getButtonStringId()));
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+            getAsserter()
+                    .withMessage("Display.getMode()")
+                    .about(MODE_SUBJECT_FACTORY)
+                    .that(display.getMode())
+                    .isEquivalentTo(new Mode(3840, 2160, 60f), REFRESH_RATE_PRECISION);
+
+             Mode[] expected2160pSupportedModes = new Mode[]{
+                    new Mode(720, 480, 60f),
+                    new Mode(720, 576, 50f),
+                    // 720p modes
+                    new Mode(1280, 720, 50f),
+                    new Mode(1280, 720, 60f),
+                    // 1080p modes
+                    new Mode(1920, 1080, 24f),
+                    new Mode(1920, 1080, 25f),
+                    new Mode(1920, 1080, 30f),
+                    new Mode(1920, 1080, 50f),
+                    new Mode(1920, 1080, 60f),
+                    // 2160p modes
+                    new Mode(3840, 2160, 24f),
+                    new Mode(3840, 2160, 25f),
+                    new Mode(3840, 2160, 30f),
+                    new Mode(3840, 2160, 50f),
+                    new Mode(3840, 2160, 60f)
+            };
+            getAsserter()
+                    .withMessage("Display.getSupportedModes()")
+                    .that(Arrays.asList(display.getSupportedModes()))
+                    .comparingElementsUsing(MODE_CORRESPONDENCE)
+                    .containsAllIn(expected2160pSupportedModes);
+        }
+    }
+
+    private class Display1080pTestStep extends SyncTestStep {
+        public Display1080pTestStep(TvAppVerifierActivity context) {
+            super(context);
+        }
+
+        @Override
+        protected String getStepName() {
+            return mContext.getString(R.string.tv_display_modes_test_step_1080p);
+        }
+
+        @Override
+        protected String getInstructionText() {
+            return mContext.getString(
+                    R.string.tv_display_modes_connect_1080p_display,
+                    mContext.getString(getButtonStringId()));
+        }
+
+        @Override
+        protected @StringRes int getButtonStringId() {
+            return R.string.tv_start_test;
+        }
+
+        @Override
+        public void runTest() {
+            Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+
+            getAsserter()
+                    .withMessage("Display.getMode()")
+                    .about(MODE_SUBJECT_FACTORY)
+                    .that(display.getMode())
+                    .isEquivalentTo(new Mode(1920, 1080, 60f), REFRESH_RATE_PRECISION);
+
+            final Mode[] expected1080pSupportedModes = new Mode[]{
+                    new Mode(720, 480, 60f),
+                    new Mode(720, 576, 50f),
+                    // 720p modes
+                    new Mode(1280, 720, 50f),
+                    new Mode(1280, 720, 60f),
+                    // 1080p modes
+                    new Mode(1920, 1080, 24f),
+                    new Mode(1920, 1080, 25f),
+                    new Mode(1920, 1080, 30f),
+                    new Mode(1920, 1080, 50f),
+                    new Mode(1920, 1080, 60f),
+            };
+            getAsserter()
+                    .withMessage("Display.getSupportedModes()")
+                    .that(Arrays.asList(display.getSupportedModes()))
+                    .comparingElementsUsing(MODE_CORRESPONDENCE)
+                    .containsAllIn(expected1080pSupportedModes);
+        }
+    }
+
+    // We use a custom Mode class since the constructors of Display.Mode are hidden. Additionally,
+    // we want to use fuzzy comparision for frame rates which is not used in Display.Mode.equals().
+    private static class Mode {
+        public int mWidth;
+        public int mHeight;
+        public float mRefreshRate;
+
+        public Mode(int width, int height, float refreshRate) {
+            this.mWidth = width;
+            this.mHeight = height;
+            this.mRefreshRate = refreshRate;
+        }
+
+        public boolean isEquivalent(Display.Mode displayMode, float refreshRatePrecision) {
+            return mHeight == displayMode.getPhysicalHeight()
+                    && mWidth == displayMode.getPhysicalWidth()
+                    && Math.abs(mRefreshRate - displayMode.getRefreshRate()) < refreshRatePrecision;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%dx%d %.2f Hz", mWidth, mHeight, mRefreshRate);
+        }
+    }
+
+    private static class ModeSubject extends Subject<ModeSubject, Display.Mode> {
+        public ModeSubject(FailureMetadata failureMetadata, @Nullable Display.Mode subject) {
+            super(failureMetadata, subject);
+        }
+
+        public void isEquivalentTo(Mode mode, float refreshRatePrecision) {
+            if (!mode.isEquivalent(actual(), refreshRatePrecision)) {
+                failWithActual("expected", mode);
+            }
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/SyncTestStep.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/SyncTestStep.java
index 1c12f0e..0c4181a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/SyncTestStep.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/SyncTestStep.java
@@ -16,8 +16,6 @@
 
 package com.android.cts.verifier.tv.display;
 
-import android.view.View;
-
 import com.android.cts.verifier.tv.TvAppVerifierActivity;
 
 /**
@@ -29,10 +27,11 @@
         super(context);
     }
 
-    public abstract boolean runTest();
+    public abstract void runTest();
 
     @Override
     protected void onButtonClickRunTest() {
-        doneWithPassingState(runTest());
+        runTest();
+        done();
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestSequence.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestSequence.java
index 093d3a2..e80828a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestSequence.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestSequence.java
@@ -4,6 +4,7 @@
 import com.android.cts.verifier.tv.TvAppVerifierActivity;
 
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * A sequence of {@link TestStepBase}s within a {@link TvAppVerifierActivity}, which are meant to be
@@ -50,6 +51,13 @@
         steps.get(0).enableButton();
     }
 
+    public String getFailureDetails() {
+        return steps.stream()
+                .filter(step -> !step.hasPassed())
+                .map(step -> step.getFailureDetails())
+                .collect(Collectors.joining("\n"));
+    }
+
     private void onAllStepsDone() {
         // The sequence passes if all containing test steps pass.
         boolean allTestStepsPass = steps.stream().allMatch(step -> step.hasPassed());
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestStepBase.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestStepBase.java
index 4de7fff..ae2a65b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestStepBase.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/display/TestStepBase.java
@@ -17,18 +17,29 @@
 package com.android.cts.verifier.tv.display;
 
 import android.view.View;
+import android.widget.TextView;
 
+import androidx.annotation.StringRes;
+
+import com.android.cts.verifier.R;
 import com.android.cts.verifier.tv.TvAppVerifierActivity;
 
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.StandardSubjectBuilder;
+
+import java.util.Arrays;
+
 /**
  * Encapsulates the logic of a test step, which displays a human instructions and a button to start
  * the test.
  */
 public abstract class TestStepBase {
     final protected TvAppVerifierActivity mContext;
-    private View mViewItem;
+    protected View mViewItem;
     private boolean mHasPassed;
     private Runnable mOnDoneListener;
+    private String mFailureDetails;
+    private StandardSubjectBuilder mAsserter;
 
     /**
      * Constructs a test step containing instruction to the user and a button.
@@ -37,6 +48,13 @@
      */
     public TestStepBase(TvAppVerifierActivity context) {
         this.mContext = context;
+
+        FailureStrategy failureStrategy = assertionError -> {
+            appendFailureDetails(assertionError.getMessage());
+            mHasPassed = false;
+        };
+        mAsserter = StandardSubjectBuilder.forCustomFailureStrategy(failureStrategy);
+        mHasPassed = true;
     }
 
     public boolean hasPassed() {
@@ -50,7 +68,10 @@
         mViewItem = mContext.createUserItem(
                 getInstructionText(),
                 getButtonStringId(),
-                (View view) -> onButtonClickRunTest());
+                (View view) -> {
+                    appendInfoDetails("Running test step %s...", getStepName());
+                    onButtonClickRunTest();
+                });
     }
 
     /**
@@ -71,6 +92,15 @@
         mOnDoneListener = listener;
     }
 
+    public String getFailureDetails() {
+        return mFailureDetails;
+    }
+
+    /**
+     * Human readable name of this test step to be output to logs.
+     */
+    protected abstract String getStepName();
+
     protected abstract void onButtonClickRunTest();
 
     /**
@@ -81,14 +111,40 @@
     /**
      * Returns id of string resource containing the text of the button.
      */
-    protected abstract int getButtonStringId();
+    protected abstract @StringRes int getButtonStringId();
 
-    protected void doneWithPassingState(boolean state) {
-        mHasPassed = state;
-        TvAppVerifierActivity.setPassState(mViewItem, state);
-
+    protected void done() {
+        TvAppVerifierActivity.setPassState(mViewItem, mHasPassed);
         if (mOnDoneListener != null) {
             mOnDoneListener.run();
         }
     }
+
+    protected StandardSubjectBuilder getAsserter() {
+        return mAsserter;
+    }
+
+    protected void appendInfoDetails(String infoFormat, Object... args) {
+        String info = String.format(infoFormat, args);
+        String details = String.format("Info: %s", info);
+        appendDetails(details);
+    }
+
+    protected void appendFailureDetails(String failure) {
+        String details = String.format("Failure: %s", failure);
+        appendDetails(details);
+        appendMessageToView(mViewItem, details);
+    }
+
+    protected void appendDetails(String details) {
+        if (mFailureDetails == null) {
+            mFailureDetails = new String();
+        }
+        mFailureDetails += details + "\n";
+    }
+
+    private static void appendMessageToView(View item, String message) {
+        TextView instructions = item.findViewById(R.id.instructions);
+        instructions.setText(instructions.getText() + "\n" + message);
+    }
 }