Merge changes Ib0cc04db,I646615fb into sc-dev am: 8ae0baa546 am: ad0d6db140

Original change: https://googleplex-android-review.googlesource.com/c/platform/platform_testing/+/16251104

Change-Id: I9c4eae184081a87f19ab7a9963f7ef13ff3983b2
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
index 530fb46..49174ab 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
@@ -151,4 +151,23 @@
      * @return true if all app names in mediaAppsNames shows up in Media Apps Grid
      */
     boolean areMediaAppsPresent(List<String> mediaAppsNames);
+
+    /**
+     * Setup expectations: "Media apps" Grid is open.
+     *
+     * @param appName App name to open
+     */
+    void openApp(String appName);
+
+    /**
+     * Setup expectations: Media app is open.
+     */
+    void openMediaAppSettingsPage();
+
+    /**
+     * Setup expectations: Media app is open. Account not logged in.
+     *
+     * @return Error message for no user login
+     */
+    String getMediaAppUserNotLoggedInErrorMessage();
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoTestMediaAppHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoTestMediaAppHelper.java
new file mode 100644
index 0000000..4ed751e
--- /dev/null
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoTestMediaAppHelper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 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.platform.helpers;
+
+public interface IAutoTestMediaAppHelper extends IAppHelper {
+    /**
+     * Loads Media files in Test Media app
+     * Setup expectations: Test Media app Settings page is open.
+     */
+    void loadMediaInLocalMediaTestApp();
+}
diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
index fb84f63..790da94 100644
--- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
+++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
@@ -222,4 +222,18 @@
      * <p>It presses YouTube PiP view twice to return to the main app.
      */
     public void backFromYouTubeFromPip();
+
+    /**
+     * Setup expectation: YouTube is on the library page.
+     *
+     * <p>presses Your videos tab.
+     */
+    public void goToYourVideos();
+
+    /**
+     * Setup expectation: YouTube is on the Your videos page.
+     *
+     * <p>presses the video name to play.
+     */
+    public void playYourVideo(String videoName);
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java
new file mode 100644
index 0000000..a94e069
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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.platform.helpers;
+
+public interface IGoogleCameraHelper2 extends IAppHelper {
+
+    /** Setup expectations: Starts taking multiple photos on camera. */
+    public void takeMultiplePhotos(int count, long takePhotoDelay);
+
+    /** Setup expectations: Switch to video record mode. */
+    public void clickVideoTab();
+
+    /** Setup expectations: Click camera video button to start recording or stop recording. */
+    public void clickCameraVideoButton();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
index 7d18138..4ebec54 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
@@ -178,4 +178,16 @@
      * @return Returns true if device is in Photos main screen, false if not.
      */
     public boolean isOnMainScreen();
+
+    /** Setup expectation: Disable backup mode in settings. */
+    public void disableBackupMode();
+
+    /** Setup expectation: Enable backup mode in settings. */
+    public void enableBackupMode();
+
+    /** Setup expectation: Verify backup starts uploading new pictures in settings. */
+    public void verifyContentStartedUploading();
+
+    /** Setup expectation: Remove photos app content. */
+    public void removeContent();
 }
diff --git a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java
index 93e6688..7d2f21c 100644
--- a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java
+++ b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java
@@ -410,7 +410,9 @@
         if (menuItemElements.size() == 0) {
             throw new UnknownUiException("Unable to find Media drop down.");
         }
-        clickAndWaitForIdleScreen(menuItemElements.get(1));
+        // Media menu drop down is the last item in Media App Screen
+        int positionOfMenuItemDropDown = menuItemElements.size() - 1;
+        clickAndWaitForIdleScreen(menuItemElements.get(positionOfMenuItemDropDown));
     }
 
     /** {@inheritDoc} */
@@ -439,4 +441,48 @@
         }
         return true;
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openApp(String appName) {
+        SystemClock.sleep(SHORT_RESPONSE_WAIT_MS); // to avoid stale object error
+        UiObject2 app = scrollAndFindUiObject(By.text(appName));
+        if (app != null) {
+            clickAndWaitForIdleScreen(app);
+        } else {
+            throw new IllegalStateException(String.format("App %s cannot be found", appName));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openMediaAppSettingsPage() {
+        List<UiObject2> mediaAppMenuItem = findUiObjects(getResourceFromConfig(
+                AutoConfigConstants.MEDIA_CENTER,
+                AutoConfigConstants.MEDIA_APP,
+                AutoConfigConstants.MEDIA_APP_DROP_DOWN_MENU));
+        if (mediaAppMenuItem.size() == 0) {
+            throw new UnknownUiException("Unable to find Media App menu items.");
+        }
+        // settings page is 2nd last item in Menu Item list
+        int settingsItemPosition = mediaAppMenuItem.size() - 2;
+        clickAndWaitForIdleScreen(mediaAppMenuItem.get(settingsItemPosition));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getMediaAppUserNotLoggedInErrorMessage() {
+        UiObject2 noLoginMsg = findUiObject(getResourceFromConfig(
+                AutoConfigConstants.MEDIA_CENTER,
+                AutoConfigConstants.MEDIA_APP,
+                AutoConfigConstants.MEDIA_APP_NO_LOGIN_MSG));
+        if (noLoginMsg == null) {
+            throw new UnknownUiException("Unable to find Media app error text.");
+        }
+        return noLoginMsg.getText();
+    }
 }
diff --git a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java
new file mode 100644
index 0000000..c74ec99
--- /dev/null
+++ b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 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.platform.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.platform.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.UiObject2;
+
+import java.util.List;
+
+public class TestMediaAppHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoTestMediaAppHelper {
+    // Wait Time
+    private static final int SHORT_RESPONSE_WAIT_MS = 1000;
+
+    public TestMediaAppHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void loadMediaInLocalMediaTestApp() {
+        // Open Account type
+        clickAndWait(
+                AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE, "Account Type");
+        // Select Paid Account type
+        clickAndWait(
+                AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE_PAID, "Account Type: Paid");
+        // open Root node type
+        clickAndWait(
+                AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE, "Root node type");
+        // select Browsable content
+        clickAndWait(AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE,
+                "Browsable Content");
+        // close settings
+        clickAndWait(AutoConfigConstants.TEST_MEDIA_APP_CLOSE_SETTING, "Close Settings");
+        selectSongInTestMediaApp();
+    }
+
+    private void selectSongInTestMediaApp() {
+        List<UiObject2> songList = findUiObjects(getResourceFromConfig(
+                AutoConfigConstants.MEDIA_CENTER,
+                AutoConfigConstants.MEDIA_APP,
+                AutoConfigConstants.MEDIA_SONGS_LIST));
+        if (songList.size() == 0) {
+            throw new UnknownUiException("Unable to find Songs in the Test Media App.");
+        }
+        clickAndWaitForIdleScreen(songList.get(1));
+        SystemClock.sleep(SHORT_RESPONSE_WAIT_MS);
+        // minimize songs
+        UiObject2 goBackToSongsList = findUiObject(getResourceFromConfig(
+                AutoConfigConstants.MEDIA_CENTER,
+                AutoConfigConstants.MEDIA_APP,
+                AutoConfigConstants.MEDIA_APP_NAVIGATION_ICON));
+        clickAndWaitForIdleScreen(goBackToSongsList);
+        SystemClock.sleep(SHORT_RESPONSE_WAIT_MS);
+    }
+
+    private void clickAndWait(String autoConfigConstants, String fieldName) {
+        UiObject2 mediaTestAppField = findUiObject(getResourceFromConfig(
+                AutoConfigConstants.MEDIA_CENTER,
+                AutoConfigConstants.TEST_MEDIA_APP,
+                autoConfigConstants));
+        if (mediaTestAppField == null) {
+            throw new UnknownUiException("Unable to find Test Media App field: " + fieldName);
+        }
+        clickAndWaitForIdleScreen(mediaTestAppField);
+        SystemClock.sleep(SHORT_RESPONSE_WAIT_MS);
+    }
+}
diff --git a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java
index c356200..a92ad34 100644
--- a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java
+++ b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java
@@ -70,11 +70,6 @@
                 getResourceFromConfig(
                         AutoConfigConstants.NOTIFICATIONS,
                         AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
-                        AutoConfigConstants.APP_NAME));
-        NOTIFICATION_REQUIRED_FIELDS.add(
-                getResourceFromConfig(
-                        AutoConfigConstants.NOTIFICATIONS,
-                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
                         AutoConfigConstants.NOTIFICATION_TITLE));
         NOTIFICATION_REQUIRED_FIELDS.add(
                 getResourceFromConfig(
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
index 161ac43..a5cf1ed 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
@@ -235,6 +235,7 @@
     // Media Center Screen
     public static final String MEDIA_CENTER_SCREEN = "MEDIA_CENTER_SCREEN";
     public static final String PLAY_PAUSE_BUTTON = "PLAY_PAUSE_BUTTON";
+    public static final String MEDIA_SONGS_LIST = "MEDIA_SONGS_LIST";
     // NEXT_BUTTON from Account Settings
     public static final String PREVIOUS_BUTTON = "PREVIOUS_BUTTON";
     public static final String SHUFFLE_BUTTON = "SHUFFLE_BUTTON";
@@ -253,4 +254,14 @@
     public static final String MEDIA_APP = "MEDIA_APP";
     public static final String MEDIA_APP_TITLE = "MEDIA_APP_TITLE";
     public static final String MEDIA_APP_DROP_DOWN_MENU = "MEDIA_APP_DROP_DOWN_MENU";
+    public static final String MEDIA_APP_NAVIGATION_ICON = "MEDIA_APP_NAVIGATION_ICON";
+    public static final String MEDIA_APP_NO_LOGIN_MSG = "MEDIA_APP_NO_LOGIN_MSG";
+    // Test Media App
+    public static final String TEST_MEDIA_APP = "TEST_MEDIA_APP";
+    public static final String TEST_MEDIA_ACCOUNT_TYPE = "TEST_MEDIA_ACCOUNT_TYPE";
+    public static final String TEST_MEDIA_ACCOUNT_TYPE_PAID = "TEST_MEDIA_ACCOUNT_TYPE_PAID";
+    public static final String TEST_MEDIA_ROOT_NODE_TYPE = "TEST_MEDIA_ROOT_NODE_TYPE";
+    public static final String TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE =
+            "TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE";
+    public static final String TEST_MEDIA_APP_CLOSE_SETTING = "TEST_MEDIA_APP_CLOSE_SETTING";
 }
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java
index f7deeae..4368d26 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java
@@ -32,6 +32,9 @@
     // Car Launcher For Reference Device
     private static final String CAR_LAUNCHER_PACKAGE = "com.android.car.carlauncher";
 
+    // TEST Media App Package
+    private static final String TEST_MEDIA_APP_PACKAGE = "com.android.car.media.testmediaapp";
+
     // Config Map
     private Map<String, AutoConfiguration> mMediaCenterConfigMap;
 
@@ -164,6 +167,9 @@
 
         // Default Media Apps UI Config
         loadDefaultMediaApp(mMediaCenterConfigMap);
+
+        // Load Test Media app UI Config
+        loadDefaultTestMediaApp(mMediaCenterConfigMap);
     }
 
     private void loadDefaultMediaCenterAppConfig(Map<String, String> mApplicationConfigMap) {
@@ -273,7 +279,50 @@
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID,
                         "car_ui_toolbar_title", MEDIA_CENTER_PACKAGE));
+        mediaAppConfiguration.addResource(
+                AutoConfigConstants.MEDIA_SONGS_LIST,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "item_container", MEDIA_CENTER_PACKAGE));
+        mediaAppConfiguration.addResource(
+                AutoConfigConstants.MEDIA_APP_NAVIGATION_ICON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_nav_icon_container", MEDIA_CENTER_PACKAGE));
+        mediaAppConfiguration.addResource(
+                AutoConfigConstants.MEDIA_APP_NO_LOGIN_MSG,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "error_message", MEDIA_CENTER_PACKAGE));
         mMediaCenterConfigMap.put(
                 AutoConfigConstants.MEDIA_APP, mediaAppConfiguration);
     }
+
+    private void loadDefaultTestMediaApp(
+            Map<String, AutoConfiguration> mMediaCenterConfigMap) {
+        AutoConfiguration testMediaAppConfiguration = new AutoConfiguration();
+        testMediaAppConfiguration.addResource(
+                AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE,
+                new AutoConfigResource(
+                        AutoConfigConstants.TEXT, "Account Type"));
+        testMediaAppConfiguration.addResource(
+                AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE_PAID,
+                new AutoConfigResource(
+                        AutoConfigConstants.TEXT, "Paid"));
+        testMediaAppConfiguration.addResource(
+                AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE,
+                new AutoConfigResource(
+                        AutoConfigConstants.TEXT, "Root node type"));
+        testMediaAppConfiguration.addResource(
+                AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.TEXT, "Only browse-able content"));
+        testMediaAppConfiguration.addResource(
+                AutoConfigConstants.TEST_MEDIA_APP_CLOSE_SETTING,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "close_target", TEST_MEDIA_APP_PACKAGE));
+        mMediaCenterConfigMap.put(
+                AutoConfigConstants.TEST_MEDIA_APP, testMediaAppConfiguration);
+    }
 }
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java
index c04c763..8447e7f 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java
@@ -183,11 +183,9 @@
         expandedNotificationsConfig.addResource(
                 AutoConfigConstants.APP_ICON,
                 new AutoConfigResource(
-                        AutoConfigConstants.RESOURCE_ID, "app_icon", SYSTEM_UI_PACKAGE));
-        expandedNotificationsConfig.addResource(
-                AutoConfigConstants.APP_NAME,
-                new AutoConfigResource(
-                        AutoConfigConstants.RESOURCE_ID, "header_text", SYSTEM_UI_PACKAGE));
+                        AutoConfigConstants.RESOURCE_ID,
+                        "notification_body_icon",
+                        SYSTEM_UI_PACKAGE));
         expandedNotificationsConfig.addResource(
                 AutoConfigConstants.NOTIFICATION_TITLE,
                 new AutoConfigResource(
diff --git a/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java
new file mode 100644
index 0000000..8647039
--- /dev/null
+++ b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is a collector helper that collects the dumpsys meminfo output for specified services and
+ * puts them into files.
+ */
+public class LyricMemProfilerHelper implements ICollectorHelper<Integer> {
+
+    private static final String TAG = LyricMemProfilerHelper.class.getSimpleName();
+
+    private static final String PID_CMD = "pgrep -f -o ";
+
+    private static final String DUMPSYS_MEMINFO_CMD = "dumpsys meminfo -s ";
+
+    private static final String DMABUF_DUMP_CMD = "dmabuf_dump";
+
+    private static final int MIN_PROFILE_PERIOD_MS = 100;
+
+    String mPidName = "camera.provider@";
+
+    // Extract value of "Native Heap:" and  "TOTAL PSS:" from command: "dumpsys meminfo -s [pid]"
+    // example of "dumpsys meminfo -s [pid]":
+    // Applications Memory Usage (in Kilobytes):
+    // Uptime: 2649336 Realtime: 3041976
+    // ** MEMINFO in pid 14612 [android.hardwar] **
+    // App Summary
+    //                       Pss(KB)                        Rss(KB)
+    //                        ------                         ------
+    //           Java Heap:        0                              0
+    //         Native Heap:   377584                         377584
+    //                Code:    79008                         117044
+    //               Stack:     3364                           3364
+    //            Graphics:    47672                          47672
+    //       Private Other:    37188
+    //              System:     5307
+    //             Unknown:                                   39136
+    //
+    //           TOTAL PSS:   550123            TOTAL RSS:   584800      TOTAL      SWAP (KB):
+    //  0
+    //
+    // Above string example will be remove "\n" first  and then extracted
+    // "377584" right after "Native Heap" and "550123" right after "TOTAL PSS"
+    // by following Regexes:
+    private static final Pattern METRIC_MEMINFO_PATTERN =
+            Pattern.compile(".+Native Heap:\\s*(\\d+)\\s*.+TOTAL PSS:\\s*(\\d+)\\s*.+");
+
+    // extrace value after "PROCESS TOTAL" in camera provider section
+    // of string from command "dmabuf_dump"
+    // Example:
+    //          PROCESS TOTAL          1752 kB          1873 kB
+    // Above string example will be extracted 1752 as group(1) and 1832 as group(2)
+    private static final Pattern METRIC_DMABUF_PSS_PATTERN =
+            Pattern.compile("\\s*PROCESS TOTAL\\s*(\\d+)\\s*kB\\s*(\\d+)\\s*kB");
+
+    // Folling Regexes is for removing "\n" in string
+    private static final Pattern REMOVE_CR_PATTERN = Pattern.compile("\n");
+
+    // This Regexes is to match string format as:
+    //   provider@2.7-se:[pid of provider]
+    //
+    // Use above pattern to find data section of camera provider
+    // of output string from command "dmabuf_dump"
+    private Pattern mDmabufProcPattern;
+
+    private UiDevice mUiDevice;
+
+    private String mCameraProviderPid = "";
+
+    private int mProfilePeriodMs = 0;
+
+    private Timer mTimer;
+
+    private static class MemInfo {
+        int mNativeHeap;
+        int mTotalPss;
+
+        public MemInfo(int nativeHeap, int totalPss) {
+            mNativeHeap = nativeHeap;
+            mTotalPss = totalPss;
+        }
+    }
+
+    private int mMaxNativeHeap;
+
+    private int mMaxTotalPss;
+
+    private int mMaxDmabuf;
+
+    @VisibleForTesting
+    protected UiDevice initUiDevice() {
+        return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    @Override
+    public boolean startCollecting() {
+        if (null == mUiDevice) {
+            mUiDevice = initUiDevice();
+        }
+        getCameraProviderPid();
+
+        // This Regexes is to match string format as:
+        //   provider@2.7-se:[pid of provider]
+        //
+        // Use following pattern to find data section of camera provider
+        // of output string from command "dmabuf_dump"
+        mDmabufProcPattern = Pattern.compile("\\s*provider@.*:" + mCameraProviderPid + "\\s*");
+
+        // To avoid frequnce of polling memory data too high and interference test case
+        // Set minimum polling period to MIN_PROFILE_PERIOD_MS (100), period profile only
+        // enable when MIN_PROFILE_PERIOD_MS <= configured polling period
+        if (MIN_PROFILE_PERIOD_MS <= mProfilePeriodMs) {
+            if (null == mTimer) {
+                mTimer = new Timer();
+                mTimer.schedule(
+                        new TimerTask() {
+                            @Override
+                            public void run() {
+                                MemInfo memInfo = processMemInfo(getMemInfoString());
+                                int dmabuf = processDmabufDump(getDmabufDumpString());
+                                mMaxNativeHeap = Math.max(mMaxNativeHeap, memInfo.mNativeHeap);
+                                mMaxTotalPss = Math.max(mMaxTotalPss, memInfo.mTotalPss);
+                                mMaxDmabuf = Math.max(mMaxDmabuf, dmabuf);
+                            }
+                        },
+                        MIN_PROFILE_PERIOD_MS,
+                        mProfilePeriodMs);
+            }
+        }
+        return true;
+    }
+
+    public void setProfilePeriodMs(int periodMs) {
+        mProfilePeriodMs = periodMs;
+    }
+
+    public void setProfilePidName(String pidName) {
+        mPidName = pidName;
+    }
+
+    @Override
+    public Map<String, Integer> getMetrics() {
+        String memInfoString = getMemInfoString();
+        String dmabufDumpString = getDmabufDumpString();
+        Map<String, Integer> metrics = processOutput(memInfoString, dmabufDumpString);
+        if (MIN_PROFILE_PERIOD_MS <= mProfilePeriodMs) {
+            metrics.put("maxNativeHeap", mMaxNativeHeap);
+            metrics.put("maxTotalPss", mMaxTotalPss);
+            metrics.put("maxDmabuf", mMaxDmabuf);
+        }
+        return metrics;
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        if (null != mTimer) {
+            mTimer.cancel();
+        }
+        return true;
+    }
+
+    private MemInfo processMemInfo(String memInfoString) {
+        int nativeHeap = 0, totalPss = 0;
+        Matcher matcher =
+                METRIC_MEMINFO_PATTERN.matcher(
+                        REMOVE_CR_PATTERN.matcher(memInfoString).replaceAll(""));
+        if (matcher.find()) {
+            nativeHeap = Integer.parseInt(matcher.group(1));
+            totalPss = Integer.parseInt(matcher.group(2));
+        } else {
+            Log.e(TAG, "Failed to collect Lyric Native Heap or TOTAL PSS metrics.");
+        }
+        return new MemInfo(nativeHeap, totalPss);
+    }
+
+    private int processDmabufDump(String dmabufDumpString) {
+        int dmabuf = 0;
+        Matcher matcher;
+        boolean procMatched = false;
+        for (String line : dmabufDumpString.split("\n")) {
+            if (procMatched) {
+                matcher = METRIC_DMABUF_PSS_PATTERN.matcher(line);
+                if (matcher.find()) {
+                    dmabuf = Integer.parseInt(matcher.group(2));
+                    break;
+                }
+            } else {
+                matcher = mDmabufProcPattern.matcher(line);
+                if (matcher.find()) {
+                    procMatched = true;
+                }
+            }
+        }
+        return dmabuf;
+    }
+
+    @VisibleForTesting
+    Map<String, Integer> processOutput(String memInfoString, String dmabufDumpString) {
+        Map<String, Integer> metrics = new HashMap<>();
+        MemInfo memInfo = processMemInfo(memInfoString);
+        int dmabuf = processDmabufDump(dmabufDumpString);
+        if (0 < memInfo.mNativeHeap) metrics.put("nativeHeap", memInfo.mNativeHeap);
+        if (0 < memInfo.mTotalPss) metrics.put("totalPss", memInfo.mTotalPss);
+        if (0 < dmabuf) metrics.put("dmabuf", dmabuf);
+        return metrics;
+    }
+
+    @VisibleForTesting
+    public String getCameraProviderPid() {
+        try {
+            mCameraProviderPid = mUiDevice.executeShellCommand(PID_CMD + mPidName).trim();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to get camera provider PID");
+            mCameraProviderPid = "";
+        }
+        return mCameraProviderPid;
+    }
+
+    @VisibleForTesting
+    public String getMemInfoString() {
+        if (!mCameraProviderPid.isEmpty()) {
+            try {
+                String cmdString = DUMPSYS_MEMINFO_CMD + mCameraProviderPid;
+                return mUiDevice.executeShellCommand(cmdString).trim();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get Mem info string ");
+            }
+        }
+        return "";
+    }
+
+    @VisibleForTesting
+    public String getDmabufDumpString() {
+        if (!mCameraProviderPid.isEmpty()) {
+            try {
+                final int minDmabufStringLen = 100;
+                final int maxDmabufRetryCount = 3;
+                String dmabufString;
+                for (int retryCount = 0; retryCount < maxDmabufRetryCount; retryCount++) {
+                    dmabufString = mUiDevice.executeShellCommand(DMABUF_DUMP_CMD).trim();
+                    // "dmabuf_dump" may not get dmabuf size information but get following string:
+                    // "debugfs entry for dmabuf not available, using /proc/<pid>/fdinfo instead"
+                    // Here use string length to detected above condition and retry.
+                    // Normal dmabuf size string should larger than 100 characters.
+                    if (minDmabufStringLen < dmabufString.length()) {
+                        return dmabufString;
+                    }
+                    Log.w(TAG, "dmabuf_dump return abnormal:" + dmabufString + ",retry");
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get DMA buf dump string");
+            }
+        }
+        return "";
+    }
+}
diff --git a/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java
new file mode 100644
index 0000000..41675bf
--- /dev/null
+++ b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import java.util.Map;
+import java.io.IOException;
+
+/**
+ * Android unit test for {@link LyricMemProfilerHelper}
+ *
+ * <p>To run: atest CollectorsHelperTest:com.android.helpers.LyricMemProfilerHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class LyricMemProfilerHelperTest {
+    private static final String TAG = LyricMemProfilerHelperTest.class.getSimpleName();
+
+    private @Mock UiDevice mUiDevice;
+
+    private int mMemInfoCmdCounter = 0;
+
+    private int mDmabufCmdCounter = 0;
+
+    private static final int MOCK_NATIVE_HEAP = 100;
+
+    private static final int MOCK_TOTAL_PSS = 200;
+
+    private static final int MOCK_DMABUF = 500;
+
+    private String genMemInfoString(int nativeHeap, int totalPss) {
+        return ".Native Heap:" + nativeHeap + " TOTAL PSS:" + totalPss + " .";
+    }
+
+    private String genDmabufString(int dmabuf, int pid) {
+        String dmabufDumpString =
+                " provider@2.7-se:"
+                        + pid
+                        + "\n"
+                        + "                  Name              Rss              Pss        "
+                        + " nr_procs            Inode\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           153387\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           153388\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           153389\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           153390\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           205579\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           205580\n"
+                        + "         PROCESS TOTAL          1752 kB          "
+                        + dmabuf
+                        + " kB\n"
+                        + "----------------------\n"
+                        + "dmabuf total: 1752 kB kernel_rss: 0 kB userspace_rss: 1752 kB"
+                        + " userspace_pss: 1752 kB";
+
+        return dmabufDumpString;
+    }
+
+    @Before
+    public void setUp() throws Throwable {
+        MockitoAnnotations.initMocks(this);
+        // Return a fake pid for our fake processes and an empty string otherwise.
+        doAnswer(
+                        (inv) -> {
+                            final int pid = 1234;
+                            String cmd = (String) inv.getArguments()[0];
+                            if (cmd.startsWith("pgrep")) {
+                                return Integer.toString(pid);
+                            } else if (cmd.startsWith("dumpsys meminfo")) {
+                                mMemInfoCmdCounter++;
+                                if (10 == mMemInfoCmdCounter) {
+                                    return genMemInfoString(
+                                            MOCK_NATIVE_HEAP + 50,
+                                            MOCK_TOTAL_PSS + 50); // max value
+                                } else {
+                                    return genMemInfoString(MOCK_NATIVE_HEAP, MOCK_TOTAL_PSS);
+                                }
+                            } else if (cmd.startsWith("dmabuf_dump")) {
+                                mDmabufCmdCounter++;
+                                if (10 == mDmabufCmdCounter) {
+                                    return genDmabufString(MOCK_DMABUF + 50, pid); // max value
+                                } else {
+                                    return genDmabufString(MOCK_DMABUF, pid);
+                                }
+                            }
+                            return "";
+                        })
+                .when(mUiDevice)
+                .executeShellCommand(any());
+    }
+
+    @Test
+    public void testParsePid() {
+        LyricMemProfilerHelper helper = new LyricMemProfilerHelper();
+        String memInfoString = helper.getMemInfoString();
+        String dmabufDumpString = helper.getDmabufDumpString();
+        // memInfo and dmabufDump get empty string due to mCameraProviderPid is empty
+        assertThat(memInfoString).isEmpty();
+        assertThat(dmabufDumpString).isEmpty();
+
+        SystemClock.sleep(1000); // sleep 1 second to wait for camera provider initialize
+        helper.startCollecting();
+        memInfoString = helper.getMemInfoString();
+        dmabufDumpString = helper.getDmabufDumpString();
+        Map<String, Integer> metrics = helper.processOutput(memInfoString, dmabufDumpString);
+
+        assertThat(metrics).containsKey("nativeHeap");
+        assertThat(metrics).containsKey("totalPss");
+        assertThat(metrics).containsKey("dmabuf");
+
+        assertThat(metrics.get("nativeHeap")).isGreaterThan(0);
+        assertThat(metrics.get("totalPss")).isGreaterThan(0);
+        assertThat(metrics.get("dmabuf")).isGreaterThan(0);
+    }
+
+    @Test
+    public void testProcessOutput() {
+        LyricMemProfilerHelper helper = new LyricMemProfilerHelper();
+        helper.startCollecting();
+        String pid = helper.getCameraProviderPid();
+        String memInfoString =
+                "Applications Memory Usage (in Kilobytes):\n"
+                        + "Uptime: 2649336 Realtime: 3041976\n"
+                        + "** MEMINFO in pid 14612 [android.hardwar] **\n"
+                        + "App Summary\n"
+                        + "                       Pss(KB)                        Rss(KB)\n"
+                        + "                        ------                         ------\n"
+                        + "           Java Heap:        0                              0\n"
+                        + "         Native Heap:   377584                         377584\n"
+                        + "                Code:    79008                         117044\n"
+                        + "               Stack:     3364                           3364\n"
+                        + "            Graphics:    47672                          47672\n"
+                        + "       Private Other:    37188\n"
+                        + "              System:     5307\n"
+                        + "             Unknown:                                   39136\n"
+                        + "\n"
+                        + "           TOTAL PSS:   550123            TOTAL RSS:   584800      TOTAL"
+                        + " SWAP (KB):        0";
+
+        String dmabufDumpString =
+                " provider@2.7-se:"
+                        + pid
+                        + "\n"
+                        + "                  Name              Rss              Pss        "
+                        + " nr_procs            Inode\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           153387\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           153388\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           153389\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           153390\n"
+                        + "             <unknown>           576 kB           576 kB               "
+                        + " 1           205579\n"
+                        + "             <unknown>             8 kB             8 kB               "
+                        + " 1           205580\n"
+                        + "         PROCESS TOTAL          1752 kB          1552 kB\n"
+                        + "----------------------\n"
+                        + "dmabuf total: 1752 kB kernel_rss: 0 kB userspace_rss: 1752 kB"
+                        + " userspace_pss: 1752 kB";
+
+        Map<String, Integer> metrics = helper.processOutput(memInfoString, dmabufDumpString);
+        assertThat(metrics.get("nativeHeap")).isEqualTo(377584);
+        assertThat(metrics.get("totalPss")).isEqualTo(550123);
+        assertThat(metrics.get("dmabuf")).isEqualTo(1552);
+
+        metrics = helper.processOutput("", "");
+        assertThat(metrics).doesNotContainKey("nativeHeap");
+        assertThat(metrics).doesNotContainKey("totalPss");
+        assertThat(metrics).doesNotContainKey("dmabuf");
+    }
+
+    @Test
+    public void testProfilePeriod() {
+        LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper();
+        helper.setProfilePeriodMs(100);
+        helper.startCollecting();
+        SystemClock.sleep(2000);
+        Map<String, Integer> metrics = helper.getMetrics();
+        helper.stopCollecting();
+        assertThat(metrics).containsKey("nativeHeap");
+        assertThat(metrics).containsKey("totalPss");
+        assertThat(metrics).containsKey("dmabuf");
+        assertThat(metrics).containsKey("maxNativeHeap");
+        assertThat(metrics).containsKey("maxTotalPss");
+        assertThat(metrics).containsKey("maxDmabuf");
+
+        assertThat(metrics.get("nativeHeap")).isLessThan(metrics.get("maxNativeHeap"));
+        assertThat(metrics.get("totalPss")).isLessThan(metrics.get("maxTotalPss"));
+        assertThat(metrics.get("dmabuf")).isLessThan(metrics.get("maxDmabuf"));
+    }
+
+    @Test
+    public void testProfilePeriodLessThanMin() {
+        LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper();
+        InOrder inOrder = inOrder(mUiDevice);
+        helper.setProfilePeriodMs(50);
+        helper.startCollecting();
+        try {
+            inOrder.verify(mUiDevice).executeShellCommand("pgrep -f -o camera.provider@");
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to execute Shell command");
+        }
+        SystemClock.sleep(3000);
+        verifyNoMoreInteractions(mUiDevice);
+
+        Map<String, Integer> metrics = helper.getMetrics();
+        helper.stopCollecting();
+        assertThat(metrics).containsKey("nativeHeap");
+        assertThat(metrics).containsKey("totalPss");
+        assertThat(metrics).containsKey("dmabuf");
+        assertThat(metrics).doesNotContainKey("maxNativeHeap");
+        assertThat(metrics).doesNotContainKey("maxTotalPss");
+        assertThat(metrics).doesNotContainKey("maxDmabuf");
+
+        assertThat(metrics.get("nativeHeap")).isEqualTo(MOCK_NATIVE_HEAP);
+        assertThat(metrics.get("totalPss")).isEqualTo(MOCK_TOTAL_PSS);
+        assertThat(metrics.get("dmabuf")).isEqualTo(MOCK_DMABUF);
+    }
+
+    @Test
+    public void testSetNewPidName() {
+        LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper();
+        InOrder inOrder = inOrder(mUiDevice);
+        final String newPidName = "new.camera.name";
+        helper.setProfilePidName(newPidName);
+        helper.startCollecting();
+        try {
+            inOrder.verify(mUiDevice).executeShellCommand("pgrep -f -o " + newPidName);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to execute Shell command");
+        }
+    }
+
+    private final class TestableLyricMemProfilerHelper extends LyricMemProfilerHelper {
+        @Override
+        protected UiDevice initUiDevice() {
+            return mUiDevice;
+        }
+    }
+}
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java
index be805c5..01ed870 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java
@@ -99,6 +99,11 @@
                         frameInfoMap);
 
                 addMetric(
+                        constructKey(KEY_PREFIX_CUJ, interactionType, "app_missed_frames"),
+                        makeLogFriendly(uiInteractionFrameInfoReported.appMissedFrames),
+                        frameInfoMap);
+
+                addMetric(
                         constructKey(KEY_PREFIX_CUJ, interactionType, SUFFIX_MAX_FRAME_MS),
                         makeLogFriendly(
                                 uiInteractionFrameInfoReported.maxFrameTimeNanos / 1000000.0),
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/DevicePropertyInfo.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/DevicePropertyInfo.java
index 7435839..e8709b3 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/DevicePropertyInfo.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/DevicePropertyInfo.java
@@ -52,6 +52,7 @@
     private final String mVersionSdk;
     private final String mVersionSecurityPatch;
     private final String mVersionIncremental;
+    private final String mBootimageFingerprint;
 
     public DevicePropertyInfo(
             String abi,
@@ -100,6 +101,58 @@
         mVersionSdk = versionSdk;
         mVersionSecurityPatch = versionSecurityPatch;
         mVersionIncremental = versionIncremental;
+        mBootimageFingerprint = "unknown";
+    }
+
+    public DevicePropertyInfo(
+            String abi,
+            String abi2,
+            String abis,
+            String abis32,
+            String abis64,
+            String board,
+            String brand,
+            String device,
+            String fingerprint,
+            String vendorFingerprint,
+            String id,
+            String manufacturer,
+            String model,
+            String product,
+            String referenceFingerprint,
+            String serial,
+            String tags,
+            String type,
+            String versionBaseOs,
+            String versionRelease,
+            String versionSdk,
+            String versionSecurityPatch,
+            String versionIncremental,
+            String bootimageFingerprint) {
+        mAbi = abi;
+        mAbi2 = abi2;
+        mAbis = abis;
+        mAbis32 = abis32;
+        mAbis64 = abis64;
+        mBoard = board;
+        mBrand = brand;
+        mDevice = device;
+        mFingerprint = fingerprint;
+        mVendorFingerprint = vendorFingerprint;
+        mId = id;
+        mManufacturer = manufacturer;
+        mModel = model;
+        mProduct = product;
+        mReferenceFingerprint = referenceFingerprint;
+        mSerial = serial;
+        mTags = tags;
+        mType = type;
+        mVersionBaseOs = versionBaseOs;
+        mVersionRelease = versionRelease;
+        mVersionSdk = versionSdk;
+        mVersionSecurityPatch = versionSecurityPatch;
+        mVersionIncremental = versionIncremental;
+        mBootimageFingerprint = bootimageFingerprint;
     }
 
     /**
@@ -120,6 +173,7 @@
         propertyMap.put(prefix + "device", mDevice);
         propertyMap.put(prefix + "fingerprint", mFingerprint);
         propertyMap.put(prefix + "vendor_fingerprint", mVendorFingerprint);
+        propertyMap.put(prefix + "bootimage_fingerprint", mBootimageFingerprint);
         propertyMap.put(prefix + "id", mId);
         propertyMap.put(prefix + "manufacturer", mManufacturer);
         propertyMap.put(prefix + "model", mModel);
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
index 8760d40..cdccb79 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
@@ -49,5 +49,5 @@
     public static final int Q = 29;
     public static final int R = 30;
     public static final int S = 31;
-
+    public static final int S_V2 = 32;
 }
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java
new file mode 100644
index 0000000..3205fa2
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 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.device.collectors;
+
+import android.os.Bundle;
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.LyricMemProfilerHelper;
+
+@OptionClass(alias = "lyric-mem-profiler-collector")
+public class LyricMemProfilerCollector extends BaseCollectionListener<Integer> {
+
+    private static final String TAG = LyricMemProfilerCollector.class.getSimpleName();
+    private static final String PROFILE_PERIOD_KEY = "profile-period-ms-enable";
+    private static final String PROFILE_PID_NAME = "profile-pid-name";
+    private LyricMemProfilerHelper mHelper = new LyricMemProfilerHelper();
+
+    public LyricMemProfilerCollector() {
+        createHelperInstance(mHelper);
+    }
+
+    /** Adds the options for total pss collector. */
+    @Override
+    public void setupAdditionalArgs() {
+        Bundle args = getArgsBundle();
+        String argString = args.getString(PROFILE_PERIOD_KEY);
+        if (argString != null) {
+            mHelper.setProfilePeriodMs(Integer.parseInt(argString));
+        }
+        argString = args.getString(PROFILE_PID_NAME);
+        if (argString != null) {
+            mHelper.setProfilePidName(argString);
+        }
+    }
+}
diff --git a/libraries/flicker/.gitignore b/libraries/flicker/.gitignore
new file mode 100644
index 0000000..caa32e6
--- /dev/null
+++ b/libraries/flicker/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+*.iml
\ No newline at end of file
diff --git a/libraries/flicker/Android.bp b/libraries/flicker/Android.bp
index 1348479..f65c6de 100644
--- a/libraries/flicker/Android.bp
+++ b/libraries/flicker/Android.bp
@@ -21,6 +21,9 @@
 java_test {
     name: "flickerlib",
     platform_apis: true,
+    optimize: {
+        enabled: false
+    },
     srcs: [
         "src/**/*.java",
         "src/**/*.kt"
@@ -34,14 +37,21 @@
 java_library {
     name: "flickerlib-core",
     platform_apis: true,
+    optimize: {
+        enabled: false
+    },
     srcs: [
         "src/com/android/server/wm/flicker/**/*.java",
         "src/com/android/server/wm/flicker/**/*.kt"
     ],
+    java_resource_dirs: [
+        "src/com/android/server/wm/flicker/service/resources/"
+    ],
     exclude_srcs: [
         "**/helpers/*",
     ],
     static_libs: [
+        "flickerlib-helpers",
         "compatibility-device-util-axt",
         "ub-uiautomator",
         "androidx.test.uiautomator_uiautomator",
@@ -51,12 +61,16 @@
         "wm-proto-parsers",
         "platform-test-annotations",
         "platform-test-core-rules",
+        "health-testing-utils",
     ],
 }
 
 java_library {
     name: "flickerlib-helpers",
     sdk_version: "test_current",
+    optimize: {
+        enabled: false
+    },
     srcs: [
         "src/**/helpers/*.java",
         "src/**/helpers/*.kt",
@@ -72,6 +86,9 @@
 java_library {
     name: "wm-proto-parsers",
     sdk_version: "test_current",
+    optimize: {
+        enabled: false
+    },
     srcs: [
         "src/com/android/server/wm/traces/**/*.java",
         "src/com/android/server/wm/traces/**/*.kt",
@@ -81,5 +98,16 @@
         "androidx.test.ext.junit",
         "platformprotosnano",
         "layersprotosnano",
+        "flicker-tags-proto",
     ],
 }
+
+java_library {
+    name: "flicker-tags-proto",
+    srcs: [
+        "**/*.proto",
+    ],
+    optimize: {
+        enabled: false
+    }
+}
diff --git a/libraries/flicker/README.md b/libraries/flicker/README.md
index 97f79b9..0b58424 100644
--- a/libraries/flicker/README.md
+++ b/libraries/flicker/README.md
@@ -111,7 +111,7 @@
 
 ## Running Tests
 
-The tests can be run as any other Android JUnit tests. `frameworks/base/tests/FlickerTests` uses the library to test common UI transitions. Run `atest FlickerTest` to execute these tests.
+The tests can be run as any other Android JUnit tests. `frameworks/base/tests/FlickerTests` uses the library to test common UI transitions. Run `atest FlickerTests` to execute these tests.
 
 ---
 
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
index 7bb2074..cc174d9 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
@@ -137,11 +137,9 @@
         val result = result
         requireNotNull(result)
 
-        val failures = result.checkAssertions(listOf(assertion))
-        val failureMessage = failures.joinToString("\n") { it.message }
-
-        if (failureMessage.isNotEmpty()) {
-            throw AssertionError(failureMessage)
+        val failures = result.checkAssertion(assertion)
+        if (failures.isNotEmpty()) {
+            throw failures.first()
         }
     }
 
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
index b43eab9..d7f7faf 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
@@ -16,7 +16,9 @@
 
 package com.android.server.wm.flicker
 
+import android.platform.test.util.TestFilter
 import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.internal.runners.statements.RunAfters
 import org.junit.runner.notification.RunNotifier
@@ -30,6 +32,16 @@
  * Implements the JUnit 4 standard test case class model, parsing from a flicker DSL.
  *
  * Supports both assertions in {@link org.junit.Test} and assertions defined in the DSL
+ *
+ * When using this runnr the default `atest class#method` command doesn't work.
+ * Instead use: -- --test-arg \
+ *     com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests:=<TEST_NAME>
+ *
+ * For example:
+ * `atest FlickerTests -- \
+ *     --test-arg com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests\
+ *     :=com.android.server.wm.flicker.close.\
+ *     CloseAppBackButtonTest#launcherWindowBecomesVisible[ROTATION_90_GESTURAL_NAV]`
  */
 class FlickerBlockJUnit4ClassRunner @JvmOverloads constructor(
     test: TestWithParameters,
@@ -42,6 +54,18 @@
     /**
      * {@inheritDoc}
      */
+    override fun getChildren(): MutableList<FrameworkMethod> {
+        val arguments = InstrumentationRegistry.getArguments()
+        val validChildren = super.getChildren().filter {
+            val childDescription = describeChild(it)
+            TestFilter.isFilteredOrUnspecified(arguments, childDescription)
+        }
+        return validChildren.toMutableList()
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     override fun classBlock(notifier: RunNotifier): Statement {
         val statement = childrenInvoker(notifier)
         val cleanUpMethod = getFlickerCleanUpMethod()
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
index 006a153..62da20d 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
@@ -18,6 +18,7 @@
 
 import com.android.server.wm.flicker.assertions.AssertionData
 import com.android.server.wm.flicker.assertions.FlickerAssertionError
+import com.android.server.wm.flicker.assertions.FlickerAssertionErrorBuilder
 import com.google.common.truth.Truth
 
 /**
@@ -51,22 +52,23 @@
     }
 
     /**
-     * Run the assertions on the trace
+     * Run the assertion on the trace
      *
-     * @throws AssertionError If the assertions fail or the transition crashed
+     * @throws AssertionError If the assertion fail or the transition crashed
      */
-    internal fun checkAssertions(
-        assertions: List<AssertionData>
-    ): List<FlickerAssertionError> {
+    internal fun checkAssertion(assertion: AssertionData): List<FlickerAssertionError> {
         checkIsExecuted()
-        val currFailures: List<FlickerAssertionError> = runs.flatMap { run ->
-            assertions.filter { it.tag == run.assertionTag }.mapNotNull { assertion ->
-                try {
-                    assertion.checkAssertion(run)
-                    null
-                } catch (error: Throwable) {
-                    FlickerAssertionError(error, assertion, run)
-                }
+        val filteredRuns = runs.filter { it.assertionTag == assertion.tag }
+        val currFailures = filteredRuns.mapNotNull { run ->
+            try {
+                assertion.checkAssertion(run)
+                null
+            } catch (error: Throwable) {
+                FlickerAssertionErrorBuilder()
+                    .fromError(error)
+                    .atTag(assertion.tag)
+                    .withTrace(run.traceFiles)
+                    .build()
             }
         }
         failures.addAll(currFailures)
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
index 5508a53..22de5b9 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
@@ -54,11 +54,11 @@
     /**
      * Truth subject that corresponds to a [WindowManagerTrace] or [WindowManagerState]
      */
-    private val wmSubject: FlickerSubject?,
+    internal val wmSubject: FlickerSubject?,
     /**
      * Truth subject that corresponds to a [LayersTrace] or [LayerTraceEntry]
      */
-    private val layersSubject: FlickerSubject?,
+    internal val layersSubject: FlickerSubject?,
     /**
      * Truth subject that corresponds to a list of [FocusEvent]
      */
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
index 3f63df7..dc10c00 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
@@ -18,7 +18,9 @@
 
 import android.util.Log
 import com.android.server.wm.flicker.monitor.ITransitionMonitor
-import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.common.ConditionList
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.parser.DeviceDumpParser
 import com.android.server.wm.traces.parser.getCurrentState
 import java.io.IOException
 import java.nio.file.Files
@@ -81,6 +83,10 @@
      * @param flicker test specification
      */
     internal open fun run(flicker: Flicker): FlickerResult {
+        val uiStableCondition = ConditionList(listOf(
+            WindowManagerConditionsFactory.isWMStateComplete(),
+            WindowManagerConditionsFactory.hasLayersAnimating().negate()
+        ))
         val runs = mutableListOf<FlickerRunResult>()
         var executionError: Throwable? = null
         try {
@@ -89,10 +95,12 @@
                 for (iteration in 0 until flicker.repetitions) {
                     try {
                         flicker.runSetup.forEach { it.invoke(flicker) }
+                        flicker.wmHelper.waitFor(uiStableCondition)
                         flicker.traceMonitors.forEach { it.start() }
                         flicker.frameStatsMonitor?.run { start() }
                         flicker.transitions.forEach { it.invoke(flicker) }
                     } finally {
+                        flicker.wmHelper.waitFor(uiStableCondition)
                         flicker.traceMonitors.forEach { it.tryStop() }
                         flicker.frameStatsMonitor?.run { tryStop() }
                         flicker.runTeardown.forEach { it.invoke(flicker) }
@@ -160,7 +168,7 @@
         tags.add(tag)
 
         val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation)
-        val deviceState = DeviceStateDump.fromDump(deviceStateBytes.first, deviceStateBytes.second)
+        val deviceState = DeviceDumpParser.fromDump(deviceStateBytes.first, deviceStateBytes.second)
         try {
             val wmTraceFile = flicker.outputDir.resolve(
                 getTaggedFilePath(flicker, tag, "wm_trace"))
@@ -176,8 +184,8 @@
 
             val result = builder.buildStateResult(
                 tag,
-                deviceState.wmTrace,
-                deviceState.layersTrace
+                deviceState.wmState?.asTrace(),
+                deviceState.layerState?.asTrace()
             )
             tagsResults.add(result)
         } catch (e: IOException) {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt
index fff00c5..9919af5 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt
@@ -19,7 +19,9 @@
 import android.platform.test.rule.NavigationModeRule
 import android.platform.test.rule.PressHomeRule
 import android.platform.test.rule.UnlockScreenRule
+import com.android.server.wm.flicker.helpers.SampleAppHelper
 import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule
+import com.android.server.wm.flicker.rules.LaunchAppRule
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import org.junit.rules.RuleChain
 import org.junit.runner.Description
@@ -33,12 +35,26 @@
 class TransitionRunnerWithRules(private val testConfig: Map<String, Any?>) : TransitionRunner() {
     private var result: FlickerResult? = null
 
-    private fun buildDefaultSetupRules(): RuleChain {
-        return RuleChain.outerRule(ChangeDisplayOrientationRule(testConfig.startRotation))
-            .around(RemoveAllTasksButHomeRule())
+    /**
+     * Create the default flicker test setup rules. In order:
+     *   - unlock device
+     *   - change orientation
+     *   - change navigation mode
+     *   - launch an app
+     *   - remove all apps
+     *   - go to home screen
+     *
+     * (b/186740751) An app should be launched because, after changing the navigation mode,
+     * the first app launch is handled as a screen size change (similar to a rotation), this
+     * causes different problems during testing (e.g. IME now shown on app launch)
+     */
+    private fun buildDefaultSetupRules(flicker: Flicker): RuleChain {
+        return RuleChain.outerRule(UnlockScreenRule())
             .around(NavigationModeRule(testConfig.navBarMode))
+            .around(LaunchAppRule(SampleAppHelper(flicker.instrumentation)))
+            .around(RemoveAllTasksButHomeRule())
+            .around(ChangeDisplayOrientationRule(testConfig.startRotation))
             .around(PressHomeRule())
-            .around(UnlockScreenRule())
     }
 
     private fun buildTransitionRule(flicker: Flicker): Statement {
@@ -54,7 +70,7 @@
     }
 
     private fun buildTransitionChain(flicker: Flicker): Statement {
-        val setupRules = buildDefaultSetupRules()
+        val setupRules = buildDefaultSetupRules(flicker)
         val transitionRule = buildTransitionRule(flicker)
         return setupRules.apply(transitionRule, Description.EMPTY)
     }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt
index 79f6298..5ac3a12 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt
@@ -18,7 +18,6 @@
 
 /**
  * The group annotations enable to run tests in parallel according to the arguments of test runner.
- * By default, the test without group annotation are considered to be in this group.
  */
 @Retention(AnnotationRetention.RUNTIME)
 @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt
index e67fe07..98c4676 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt
@@ -18,7 +18,6 @@
 
 /**
  * The group annotations enable to run tests in parallel according to the arguments of test runner.
- * By default, the test without group annotation are considered to be in this group.
  */
 @Retention(AnnotationRetention.RUNTIME)
 @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt
new file mode 100644
index 0000000..c1cdcfc
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.annotation
+
+/**
+ * The group annotations enable to run tests in parallel according to the arguments of test runner.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class Group4
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt
index c384760..cbb4f11 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt
@@ -16,13 +16,14 @@
 
 package com.android.server.wm.flicker.assertions
 
+import androidx.annotation.VisibleForTesting
 import com.android.server.wm.flicker.FlickerRunResult
 import kotlin.reflect.KClass
 
 /**
  * Class containing basic data about a trace assertion for Flicker DSL
  */
-data class AssertionData internal constructor(
+data class AssertionData @VisibleForTesting constructor(
     /**
      * Segment of the trace where the assertion will be applied (e.g., start, end).
      */
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt
index 26e4404..5f1471a 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt
@@ -26,39 +26,61 @@
 /**
  * Utility class to store assertions with an identifier to help generate more useful debug data
  * when dealing with multiple assertions.
+ *
+ * @param assertion Assertion to execute
+ * @param name Assertion name
+ * @param isOptional If the assertion is optional (can fail) or not (must pass)
  */
 open class NamedAssertion<T> (
     private val assertion: Assertion<T>,
-    open val name: String
+    open val name: String,
+    open val isOptional: Boolean = false
 ) : Assertion<T> {
     override fun invoke(target: T): Unit = assertion.invoke(target)
 
-    override fun toString(): String = "Assertion($name)"
+    override fun toString(): String = "Assertion($name)${if (isOptional) "[optional]" else ""}"
 }
 
 /**
  * Utility class to store assertions composed of multiple individual assertions
  */
-class CompoundAssertion<T>(assertion: Assertion<T>, name: String) :
+class CompoundAssertion<T>(assertion: Assertion<T>, name: String, optional: Boolean) :
     NamedAssertion<T>(assertion, name) {
     private val assertions = mutableListOf<NamedAssertion<T>>()
 
     init {
-        add(assertion, name)
+        add(assertion, name, optional)
     }
 
+    override val isOptional: Boolean
+        get() = assertions.all { it.isOptional }
+
     override val name: String
         get() = assertions.joinToString(" and ") { it.name }
 
     /**
      * Executes all [assertions] on [target]
+     *
+     * In case of failure, returns the first non-optional failure (if available)
+     * or the first failed assertion
      */
     override fun invoke(target: T) {
-        val failure = assertions.mapNotNull {
-            kotlin.runCatching { it.invoke(target) }.exceptionOrNull()
-        }.firstOrNull()
-        if (failure != null) {
-            throw failure
+        val failures = assertions
+            .mapNotNull { assertion ->
+                val error = kotlin.runCatching { assertion.invoke(target) }.exceptionOrNull()
+                if (error != null) {
+                    Pair(assertion, error)
+                } else {
+                    null
+                }
+            }
+        val nonOptionalFailure = failures.firstOrNull { !it.first.isOptional }
+        if (nonOptionalFailure != null) {
+            throw nonOptionalFailure.second
+        }
+        val firstFailure = failures.firstOrNull()
+        if (firstFailure != null) {
+            throw firstFailure.second
         }
     }
 
@@ -67,7 +89,7 @@
     /**
      * Adds a new assertion to the list
      */
-    fun add(assertion: Assertion<T>, name: String) {
-        assertions.add(NamedAssertion(assertion, name))
+    fun add(assertion: Assertion<T>, name: String, optional: Boolean) {
+        assertions.add(NamedAssertion(assertion, name, optional))
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt
index d3fb579..a714edc 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt
@@ -16,6 +16,8 @@
 
 package com.android.server.wm.flicker.assertions
 
+import android.util.Log
+import com.android.server.wm.flicker.FLICKER_TAG
 import com.google.common.truth.Fact
 import kotlin.math.max
 
@@ -31,15 +33,15 @@
     private val assertions = mutableListOf<CompoundAssertion<T>>()
     private var skipUntilFirstAssertion = false
 
-    fun add(name: String, assertion: Assertion<T>) {
-        assertions.add(CompoundAssertion(assertion, name))
+    fun add(name: String, isOptional: Boolean = false, assertion: Assertion<T>) {
+        assertions.add(CompoundAssertion(assertion, name, isOptional))
     }
 
     /**
      * Append [assertion] to the last existing set of assertions.
      */
-    fun append(name: String, assertion: Assertion<T>) {
-        assertions.last().add(assertion, name)
+    fun append(name: String, isOptional: Boolean = false, assertion: Assertion<T>) {
+        assertions.last().add(assertion, name, isOptional)
     }
 
     /**
@@ -73,19 +75,31 @@
         var entryIndex = 0
         var assertionIndex = 0
         var lastPassedAssertionIndex = -1
+        val assertionTrace = mutableListOf<String>()
         while (assertionIndex < assertions.size && entryIndex < entries.size) {
             val currentAssertion = assertions[assertionIndex]
             val currEntry = entries[entryIndex]
             try {
+                val log = "${assertionIndex + 1}/${assertions.size}:[${currentAssertion.name}]\t" +
+                        "Entry: ${entryIndex + 1}/${entries.size} $currEntry"
+                Log.v(FLICKER_TAG, "Checking Assertion: $log")
+                assertionTrace.add(log)
                 currentAssertion.invoke(currEntry)
                 lastPassedAssertionIndex = assertionIndex
                 entryIndex++
             } catch (e: Throwable) {
+                // ignore errors are the start of the trace
                 val ignoreFailure = skipUntilFirstAssertion && lastPassedAssertionIndex == -1
                 if (ignoreFailure) {
                     entryIndex++
                     continue
                 }
+                // failure is an optional assertion, just consider it passed skip it
+                if (currentAssertion.isOptional) {
+                    lastPassedAssertionIndex = assertionIndex
+                    assertionIndex++
+                    continue
+                }
                 if (lastPassedAssertionIndex != assertionIndex) {
                     val prevEntry = entries[max(entryIndex - 1, 0)]
                     prevEntry.fail(e)
@@ -97,17 +111,23 @@
                 }
             }
         }
+        // Didn't pass any assertions
         if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty() && failures.isEmpty()) {
             entries.first().fail("Assertion never passed", assertions.first())
         }
 
-        if (failures.isEmpty() && assertionIndex != assertions.lastIndex) {
-            val reason = listOf(
-                Fact.fact("Assertion never became false", assertions[assertionIndex]),
-                Fact.fact("Passed assertions", assertions.take(assertionIndex).joinToString(",")),
-                Fact.fact("Untested assertions",
-                    assertions.drop(assertionIndex + 1).joinToString(","))
-            )
+        val untestedAssertions = assertions.drop(assertionIndex + 1)
+        if (failures.isEmpty() && untestedAssertions.any { !it.isOptional }) {
+            val passedAssertionsFacts = assertions.take(assertionIndex)
+                    .map { Fact.fact("Passed", it) }
+            val untestedAssertionsFacts = untestedAssertions
+                    .map { Fact.fact("Untested", it) }
+            val trace = assertionTrace.map { Fact.fact("Trace", it) }
+            val reason = mutableListOf<Fact>()
+            reason.addAll(passedAssertionsFacts)
+            reason.add(Fact.fact("Assertion never failed", assertions[assertionIndex]))
+            reason.addAll(untestedAssertionsFacts)
+            reason.addAll(trace)
             entries.first().fail(reason)
         }
     }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
index 9061a6a..7947a6f 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
@@ -16,47 +16,11 @@
 
 package com.android.server.wm.flicker.assertions
 
-import com.android.server.wm.flicker.FlickerRunResult
-import com.android.server.wm.flicker.traces.FlickerSubjectException
 import java.nio.file.Path
 import kotlin.AssertionError
 
 class FlickerAssertionError(
-    cause: Throwable,
-    @JvmField val assertion: AssertionData,
-    @JvmField val iteration: Int,
-    @JvmField val assertionTag: String,
-    @JvmField val traceFiles: List<Path>
-) : AssertionError(cause) {
-    constructor(cause: Throwable, assertion: AssertionData, run: FlickerRunResult)
-        : this(cause, assertion, run.iteration, run.assertionTag, run.traceFiles)
-
-    override val message: String
-        get() = buildString {
-            append("\n")
-            append("Test failed")
-            append("\n")
-            append("Iteration: ")
-            append(iteration)
-            append("\n")
-            append("Tag: ")
-            append(assertionTag)
-            append("\n")
-            append("Files: ")
-            append("\n")
-            traceFiles.forEach {
-                append("\t")
-                append(it)
-                append("\n")
-            }
-            // For subject exceptions, add the facts (layer/window/entry/etc)
-            // and the original cause of failure
-            if (cause is FlickerSubjectException) {
-                append(cause.facts)
-                append("\n")
-                cause.cause?.message?.let { append(it) }
-            } else {
-                cause?.message?.let { append(it) }
-            }
-        }
-}
\ No newline at end of file
+    message: String,
+    cause: Throwable?,
+    val traceFiles: List<Path>
+) : AssertionError(message, cause)
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt
new file mode 100644
index 0000000..74f32da
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.assertions
+
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.google.common.truth.Fact
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import java.nio.file.Path
+
+class FlickerAssertionErrorBuilder {
+    private var error: Throwable? = null
+    private var traceFiles: List<Path> = emptyList()
+    private var tag = ""
+
+    fun fromError(error: Throwable): FlickerAssertionErrorBuilder = apply {
+        this.error = error
+    }
+
+    fun withTrace(traceFiles: List<Path>): FlickerAssertionErrorBuilder = apply {
+        this.traceFiles = traceFiles
+    }
+
+    fun atTag(tag: String): FlickerAssertionErrorBuilder = apply {
+        this.tag = when (tag) {
+            AssertionTag.START -> "before transition (initial state)"
+            AssertionTag.END -> "after transition (final state)"
+            AssertionTag.ALL -> "during transition"
+            else -> "at user-defined location ($tag)"
+        }
+    }
+
+    fun build(): FlickerAssertionError {
+        return FlickerAssertionError(errorMessage, rootCause, traceFiles)
+    }
+
+    private val errorMessage get() = buildString {
+        val error = error
+        requireNotNull(error)
+        if (error is FlickerSubjectException) {
+            appendln(error.errorType)
+            appendln()
+            append(error.errorDescription)
+            appendln()
+            append(error.subjectInformation)
+            append("\t").appendln(Fact.fact("Location", tag))
+            appendln()
+        } else {
+            appendln(error.message)
+        }
+        appendln("Trace files:")
+        append(traceFileMessage)
+        appendln()
+        appendln("Cause:")
+        append(rootCauseStackTrace)
+        appendln()
+        appendln("Full stacktrace:")
+        appendln()
+    }
+
+    private val traceFileMessage get() = buildString {
+        traceFiles.forEach {
+            append("\t")
+            appendln(it)
+        }
+    }
+
+    private val rootCauseStackTrace: String get() {
+        val rootCause = rootCause
+        return if (rootCause != null) {
+            val baos = ByteArrayOutputStream()
+            PrintStream(baos, true)
+                    .use { ps -> rootCause.printStackTrace(ps) }
+            "\t$baos"
+        } else {
+            ""
+        }
+    }
+
+    /**
+     * In some paths the exceptions are encapsulated by the Truth subjects
+     * To make sure the correct error is printed, located the first non-subject
+     * related exception and use that as cause.
+     */
+    private val rootCause: Throwable? get() {
+        var childCause: Throwable? = this.error?.cause
+        if (childCause != null && childCause is FlickerSubjectException) {
+            childCause = childCause.cause
+        }
+        return childCause
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
index d2f956f..c54bc9d 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.assertions
 
+import androidx.annotation.VisibleForTesting
 import com.android.server.wm.flicker.traces.FlickerSubjectException
 import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
@@ -29,8 +30,20 @@
     protected val fm: FailureMetadata,
     data: Any?
 ) : Subject(fm, data) {
-    abstract val defaultFacts: String
     abstract fun clone(): FlickerSubject
+    @VisibleForTesting
+    abstract val timestamp: Long
+    protected abstract val parent: FlickerSubject?
+
+    protected abstract val selfFacts: List<Fact>
+    val completeFacts: List<Fact> get() {
+        val facts = selfFacts.toMutableList()
+        parent?.run {
+            val ancestorFacts = this.completeFacts
+            facts.addAll(ancestorFacts)
+        }
+        return facts
+    }
 
     /**
      * Fails an assertion on a subject
@@ -95,4 +108,10 @@
      * Necessary because check is protected and final in the Truth library
      */
     fun verify(message: String): StandardSubjectBuilder = check(message)
-}
\ No newline at end of file
+
+    companion object {
+        @VisibleForTesting
+        @JvmStatic
+        val ASSERTION_TAG = "Assertion"
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt
index fe7058d..9113b2d 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt
@@ -24,6 +24,7 @@
 import android.os.SystemClock
 import android.util.Log
 import android.util.Rational
+import android.view.Display
 import android.view.Surface
 import android.view.View
 import android.view.ViewConfiguration
@@ -35,7 +36,8 @@
 import androidx.test.uiautomator.Until
 import com.android.compatibility.common.util.SystemUtil
 import com.android.server.wm.flicker.helpers.WindowUtils.displayBounds
-import com.android.server.wm.flicker.helpers.WindowUtils.getNavigationBarPosition
+import com.android.server.wm.flicker.helpers.WindowUtils.estimateNavigationBarPosition
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import org.junit.Assert
 import org.junit.Assert.assertNotNull
@@ -112,7 +114,7 @@
             navBar.visibleBounds
         } else {
             Log.e(TAG, "Could not find nav bar, infer location")
-            getNavigationBarPosition(Surface.ROTATION_0).bounds
+            estimateNavigationBarPosition(Surface.ROTATION_0).bounds
         }
 
         val startX = navBarVisibleBounds.centerX()
@@ -151,8 +153,11 @@
         recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
     }
     assertNotNull("Recent items didn't appear", recents)
-    wmHelper.waitForNavBarStatusBarVisible()
-    wmHelper.waitForAppTransitionIdle()
+    wmHelper.waitFor(
+        WindowManagerConditionsFactory.isNavBarVisible(),
+        WindowManagerConditionsFactory.isStatusBarVisible(),
+        WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)
+    )
 }
 
 private fun getLauncherOverviewSelector(device: UiDevice): BySelector {
@@ -242,7 +247,9 @@
 
     // Wait for animation to complete.
     this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
-    wmHelper.waitForSurfaceAppeared(DOCKED_STACK_DIVIDER)
+    wmHelper.waitFor(
+        WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER),
+        WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
 
     if (!this.isInSplitScreen()) {
         Assert.fail("Unable to find Split screen divider")
@@ -270,8 +277,10 @@
     return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null
 }
 
-fun UiDevice.waitSplitScreenGone(): Boolean {
-    return this.wait(Until.gone(splitScreenDividerSelector), FIND_TIMEOUT) != null
+fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper): Boolean {
+    return wmHelper.waitFor(
+        WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER),
+        WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
 }
 
 private val splitScreenDividerSelector: BySelector
@@ -303,7 +312,7 @@
  *
  * @throws AssertionError when unable to find the split screen divider
  */
-fun UiDevice.exitSplitScreenFromBottom() {
+fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) {
     // Quickstep enabled
     val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
     assertNotNull("Unable to find Split screen divider", divider)
@@ -315,7 +324,7 @@
         Point(this.displayWidth / 2, this.displayHeight)
     }
     divider.drag(dstPoint, 400)
-    if (!this.waitSplitScreenGone()) {
+    if (!waitSplitScreenGone(wmHelper)) {
         Assert.fail("Split screen divider never disappeared")
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt
new file mode 100644
index 0000000..940c063
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import com.android.server.wm.traces.common.FlickerComponentName
+
+/**
+ * Helper to launch the default browser app (compatible with AOSP)
+ *
+ * This helper has no other functionality but the app launch.
+ *
+ * This helper is used to launch an app after some operations (e.g., navigation mode change),
+ * so that the device is stable before executing flicker tests
+ */
+class SampleAppHelper(
+    instrumentation: Instrumentation,
+    private val pkgManager: PackageManager = instrumentation.context.packageManager
+) : StandardAppHelper(
+    instrumentation,
+    "SampleApp",
+    getBrowserComponent(pkgManager)
+) {
+    override fun getOpenAppIntent(): Intent =
+        pkgManager.getLaunchIntentForPackage(component.packageName)
+            ?: error("Unable to find intent for browser")
+
+    companion object {
+        private fun getBrowserIntent(): Intent {
+            val intent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            return intent
+        }
+
+        private fun getBrowserComponent(pkgManager: PackageManager): FlickerComponentName {
+            val intent = getBrowserIntent()
+            val resolveInfo = pkgManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
+                    ?: error("Unable to resolve browser activity")
+            return FlickerComponentName(resolveInfo.activityInfo.packageName, className = "")
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
index 71338f1..20ebfde 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
@@ -28,9 +28,8 @@
 import androidx.test.uiautomator.BySelector
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.windowmanager.WindowManagerState
-import com.android.server.wm.traces.parser.toActivityName
-import com.android.server.wm.traces.parser.toWindowName
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
 /**
@@ -40,7 +39,7 @@
 open class StandardAppHelper @JvmOverloads constructor(
     instr: Instrumentation,
     @JvmField val appName: String,
-    @JvmField val component: ComponentName,
+    @JvmField val component: FlickerComponentName,
     protected val launcherStrategy: ILauncherStrategy =
         LauncherStrategyFactory.getInstance(instr).launcherStrategy
 ) : AbstractStandardAppHelper(instr) {
@@ -52,10 +51,7 @@
         launcherStrategy: ILauncherStrategy =
             LauncherStrategyFactory.getInstance(instr).launcherStrategy
     ): this(instr, appName,
-        ComponentName.createRelative(packageName, ".$activity"), launcherStrategy)
-
-    val windowName: String = component.toWindowName()
-    val activityName: String = component.toActivityName()
+        FlickerComponentName(packageName, ".$activity"), launcherStrategy)
 
     private val activityManager: ActivityManager?
         get() = mInstrumentation.context.getSystemService(ActivityManager::class.java)
@@ -88,7 +84,7 @@
         val intent = Intent()
         intent.addCategory(Intent.CATEGORY_LAUNCHER)
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        intent.component = component
+        intent.component = ComponentName(component.packageName, component.className)
         return intent
     }
 
@@ -109,7 +105,6 @@
     /**
      * Exits the activity and wait for activity destroyed
      */
-    @JvmOverloads
     fun exit(
         wmHelper: WindowManagerStateHelper
     ) {
@@ -177,10 +172,11 @@
         val window = if (expectedWindowName.isNotEmpty()) {
             expectedWindowName
         } else {
-            windowName
+            component.toWindowName()
         }
         wmHelper.waitFor("App is shown") {
-            it.wmState.isComplete() && it.wmState.isWindowVisible(window)
+            it.wmState.isComplete() && it.wmState.isWindowVisible(window) &&
+                !it.layerState.isAnimating()
         }
 
         wmHelper.waitForAppTransitionIdle()
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt
index 2d1e443..f40ca50 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt
@@ -23,8 +23,7 @@
 import android.view.Surface
 import android.view.WindowManager
 import androidx.test.platform.app.InstrumentationRegistry
-import kotlin.math.max
-import kotlin.math.min
+import com.android.server.wm.traces.common.layers.Display
 
 fun Int.isRotated() = this == Surface.ROTATION_90 || this == Surface.ROTATION_270
 
@@ -77,32 +76,54 @@
     }
 
     /**
-     * Gets the expected status bar position at a specific rotation
+     * Gets the expected status bar position for a specific display
      *
-     * @param requestedRotation Device rotation
+     * @param display the main display
      */
-    fun getStatusBarPosition(requestedRotation: Int): Region {
-        val displayBounds = displayBounds
-        val resourceName: String
-        val width: Int
-        if (!requestedRotation.isRotated()) {
-            resourceName = "status_bar_height_portrait"
-            width = min(displayBounds.width(), displayBounds.height())
+    fun getStatusBarPosition(display: Display): Region {
+        val resourceName = if (!display.transform.getRotation().isRotated()) {
+            "status_bar_height_portrait"
         } else {
-            resourceName = "status_bar_height_landscape"
-            width = max(displayBounds.width(), displayBounds.height())
+            "status_bar_height_landscape"
         }
         val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
         val height = resources.getDimensionPixelSize(resourceId)
-        return Region(0, 0, width, height)
+        return Region(0, 0, display.layerStackSpace.width, height)
     }
 
     /**
-     * Gets the expected navigation bar position at a specific rotation
+     * Gets the expected navigation bar position for a specific display
+     *
+     * @param display the main display
+     */
+    fun getNavigationBarPosition(display: Display): Region {
+        val navBarWidth = getDimensionPixelSize("navigation_bar_width")
+        val navBarHeight = navigationBarHeight
+        val displayHeight = display.layerStackSpace.height
+        val displayWidth = display.layerStackSpace.width
+        val requestedRotation = display.transform.getRotation()
+
+        return when {
+            // nav bar is at the bottom of the screen
+            requestedRotation in listOf(Surface.ROTATION_0, Surface.ROTATION_180) ||
+                    isGesturalNavigationEnabled ->
+                Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
+            // nav bar is at the right side
+            requestedRotation == Surface.ROTATION_90 ->
+                Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
+            // nav bar is at the left side
+            requestedRotation == Surface.ROTATION_270 ->
+                Region(0, 0, navBarWidth, displayHeight)
+            else -> error("Unknown rotation $requestedRotation")
+        }
+    }
+
+    /**
+     * Estimate the navigation bar position at a specific rotation
      *
      * @param requestedRotation Device rotation
      */
-    fun getNavigationBarPosition(requestedRotation: Int): Region {
+    fun estimateNavigationBarPosition(requestedRotation: Int): Region {
         val displayBounds = displayBounds
         val displayWidth: Int
         val displayHeight: Int
@@ -168,4 +189,4 @@
                     .getIdentifier("docked_stack_divider_insets", "dimen", "android")
             return resources.getDimensionPixelSize(resourceId)
         }
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt
index 6a87b51..88075e1 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt
@@ -17,10 +17,11 @@
 @file:JvmName("Extensions")
 package com.android.server.wm.flicker.monitor
 
-import com.android.server.wm.traces.parser.DeviceStateDump
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.android.server.wm.traces.common.DeviceTraceDump
 import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.parser.DeviceDumpParser
 import com.android.server.wm.traces.parser.layers.LayersTraceParser
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
 import java.nio.file.Path
@@ -73,11 +74,11 @@
 fun withTracing(
     outputDir: Path = getDefaultFlickerOutputDir(),
     predicate: () -> Unit
-): DeviceStateDump {
+): DeviceTraceDump {
     val traces = recordTraces(outputDir, predicate)
     val wmTraceData = traces.first
     val layersTraceData = traces.second
-    return DeviceStateDump.fromTrace(wmTraceData, layersTraceData)
+    return DeviceDumpParser.fromTrace(wmTraceData, layersTraceData)
 }
 
 /**
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt
index 8e5a716..45ffcda 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt
@@ -39,12 +39,6 @@
             save("${testTag}_$iteration", flickerRunResultBuilder)
 
     /**
-     * Saves any monitor artifacts to file adding `testTag` to the file name.
-     *
-     * @param testTag suffix added to artifact name
-     * @return Path to saved artifact
-     */
-    /**
      * Saves trace file to the external storage directory suffixing the name with the testtag and
      * iteration.
      *
@@ -58,7 +52,4 @@
     fun save(testTag: String, flickerRunResultBuilder: FlickerRunResult.Builder) {
         throw UnsupportedOperationException("Save not implemented for this monitor")
     }
-
-    val checksum: String
-        get() = throw UnsupportedOperationException("Checksum not implemented for this monitor")
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt
index 74fa388..b9f16cd 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt
@@ -31,7 +31,7 @@
 open class LayersTraceMonitor(
     outputDir: Path,
     private val traceFlags: Int
-) : TransitionMonitor(outputDir, "layers_trace.pb") {
+) : TransitionMonitor(outputDir, "layers_trace$WINSCOPE_EXT") {
 
     constructor(outputDir: Path) : this(outputDir, TRACE_FLAGS)
 
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt
index 9240995..d941833 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt
@@ -19,14 +19,8 @@
 import androidx.annotation.VisibleForTesting
 import com.android.compatibility.common.util.SystemUtil
 import com.android.server.wm.flicker.FlickerRunResult
-import com.google.common.io.BaseEncoding
-import java.io.FileInputStream
-import java.io.IOException
-import java.nio.ByteBuffer
 import java.nio.file.Files
 import java.nio.file.Path
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
 
 /**
  * Base class for monitors containing common logic to read the trace as a byte array and save the
@@ -36,9 +30,6 @@
     @VisibleForTesting var outputPath: Path,
     protected var sourceTraceFilePath: Path
 ) : ITransitionMonitor {
-    override var checksum: String = ""
-        protected set
-
     abstract val isEnabled: Boolean
 
     abstract fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path)
@@ -50,8 +41,6 @@
         require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" }
 
         setResult(flickerRunResultBuilder, savedTrace)
-
-        checksum = calculateChecksum(savedTrace)
     }
 
     fun save(testTag: String) {
@@ -59,8 +48,6 @@
         val savedTrace = outputPath.resolve("${testTag}_${sourceTraceFilePath.fileName}")
         moveFile(sourceTraceFilePath, savedTrace)
         require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" }
-
-        checksum = calculateChecksum(savedTrace)
     }
 
     private fun moveFile(src: Path, dst: Path) {
@@ -74,28 +61,4 @@
         SystemUtil.runShellCommand("chmod a+r $dst")
         SystemUtil.runShellCommand("rm $src")
     }
-
-    companion object {
-        @VisibleForTesting
-        @JvmStatic
-        fun calculateChecksum(traceFile: Path): String {
-            return try {
-                val messageDigest = MessageDigest.getInstance("SHA-256")
-                val inputStream = FileInputStream(traceFile.toFile())
-                val channel = inputStream.channel
-                val buffer = ByteBuffer.allocate(2048)
-                while (channel.read(buffer) != -1) {
-                    buffer.flip()
-                    messageDigest.update(buffer)
-                    buffer.clear()
-                }
-                val hash = messageDigest.digest()
-                BaseEncoding.base16().encode(hash).toLowerCase()
-            } catch (e: NoSuchAlgorithmException) {
-                throw IllegalArgumentException("Checksum algorithm SHA-256 not found", e)
-            } catch (e: IOException) {
-                throw IllegalArgumentException("File not found", e)
-            }
-        }
-    }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt
index 3424aa2..3df8007 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt
@@ -61,5 +61,6 @@
 
     companion object {
         private val TRACE_DIR = Paths.get("/data/misc/wmtrace/")
+        internal const val WINSCOPE_EXT = ".winscope"
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt
index 0e46c81..11858cb 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt
@@ -30,7 +30,7 @@
  */
 open class WindowManagerTraceMonitor(
     outputDir: Path
-) : TransitionMonitor(outputDir, "wm_trace.pb") {
+) : TransitionMonitor(outputDir, "wm_trace$WINSCOPE_EXT") {
     private val windowManager = WindowManagerGlobal.getWindowManagerService()
     override fun start() {
         try {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt
new file mode 100644
index 0000000..d425775
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.rules
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.helpers.StandardAppHelper
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Launched an app before the test
+ *
+ * @param instrumentation Instrumentation mechanism to use
+ * @param wmHelper WM/SF synchronization helper
+ * @param appHelper App to launch
+ */
+class LaunchAppRule @JvmOverloads constructor(
+    private val appHelper: StandardAppHelper,
+    private val instrumentation: Instrumentation = appHelper.mInstrumentation,
+    private val wmHelper: WindowManagerStateHelper = WindowManagerStateHelper()
+) : TestWatcher() {
+    @JvmOverloads
+    constructor(
+        component: FlickerComponentName,
+        appName: String = "",
+        instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+        wmHelper: WindowManagerStateHelper = WindowManagerStateHelper()
+    ): this(StandardAppHelper(instrumentation, appName, component), instrumentation, wmHelper)
+
+    override fun starting(description: Description?) {
+        appHelper.launchViaIntent()
+        appHelper.exit(wmHelper)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt
new file mode 100644
index 0000000..165fa86
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.rules
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.android.server.wm.flicker.monitor.LayersTraceMonitor
+import com.android.server.wm.flicker.monitor.ScreenRecorder
+import com.android.server.wm.flicker.monitor.TraceMonitor
+import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor
+import com.android.server.wm.flicker.service.FlickerService
+import com.android.server.wm.flicker.service.FlickerService.Companion.getFassFilePath
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Collect the WM and SF traces, parse them and call the WM Flicker Service after the test
+ */
+open class WMFlickerServiceRule @JvmOverloads constructor(
+    private val outputDir: Path = getDefaultFlickerOutputDir()
+) : TestWatcher() {
+    private val traceMonitors = mutableListOf<TraceMonitor>()
+
+    protected var wmTrace: WindowManagerTrace = WindowManagerTrace(emptyArray(), source = "")
+    protected var layersTrace: LayersTrace = LayersTrace(emptyArray(), source = "")
+
+    override fun starting(description: Description?) {
+        setupMonitors()
+        cleanupTraceFiles()
+        traceMonitors.forEach {
+            it.start()
+        }
+    }
+
+    override fun finished(description: Description?) {
+        val testTag = description?.methodName ?: "fass"
+        traceMonitors.forEach {
+            it.stop()
+            it.save(testTag)
+        }
+
+        Files.createDirectories(outputDir)
+        wmTrace = getWindowManagerTrace(getFassFilePath(outputDir, testTag, "wm_trace"))
+        layersTrace = getLayersTrace(getFassFilePath(outputDir, testTag, "layers_trace"))
+
+        val flickerService = FlickerService()
+        flickerService.process(wmTrace, layersTrace, outputDir, testTag)
+    }
+
+    private fun setupMonitors() {
+        traceMonitors.add(WindowManagerTraceMonitor(outputDir))
+        traceMonitors.add(LayersTraceMonitor(outputDir))
+        traceMonitors.add(ScreenRecorder(
+            outputDir,
+            InstrumentationRegistry.getInstrumentation().targetContext)
+        )
+    }
+
+    /**
+     * Remove the WM trace and layers trace files collected from previous test runs.
+     */
+    private fun cleanupTraceFiles() {
+        Files.list(outputDir).forEach { file ->
+            if (!Files.isDirectory(file)) {
+                Files.delete(file)
+            }
+        }
+    }
+
+    /**
+     * Parse the window manager trace file.
+     *
+     * @param traceFilePath
+     * @return parsed window manager trace.
+     */
+    private fun getWindowManagerTrace(traceFilePath: Path): WindowManagerTrace {
+        val wmTraceByteArray: ByteArray = Files.readAllBytes(traceFilePath)
+        return WindowManagerTraceParser.parseFromTrace(wmTraceByteArray)
+    }
+
+    /**
+     * Parse the layers trace file.
+     *
+     * @param traceFilePath
+     * @return parsed layers trace.
+     */
+    private fun getLayersTrace(traceFilePath: Path): LayersTrace {
+        val layersTraceByteArray: ByteArray = Files.readAllBytes(traceFilePath)
+        return LayersTraceParser.parseFromTrace(layersTraceByteArray)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt
new file mode 100644
index 0000000..5c3606d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.rules
+
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.service.FlickerService
+import com.android.server.wm.flicker.service.assertors.AssertionConfigParser
+import com.android.server.wm.flicker.service.assertors.AssertionData
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import org.junit.rules.TestWatcher
+
+/**
+ * A test rule reusing flicker data from [FlickerTestParameter], and fetching the traces
+ * to call the WM Flicker Service after the test
+ */
+@Deprecated("This test rule should be only used with legacy flicker tests. " +
+    "For new tests use WMFlickerServiceRule instead")
+class WMFlickerServiceRuleForTestSpec(
+    private val testSpec: FlickerTestParameter
+) : TestWatcher() {
+    private fun checkFlicker(category: String): List<ErrorTrace> {
+        // run flicker if it was not executed before
+        testSpec.flicker.result ?: testSpec.assertWm { isNotEmpty() }
+
+        val errors = mutableListOf<ErrorTrace>()
+        val result = testSpec.flicker.result ?: error("No flicker results for ${testSpec.flicker}")
+        val assertions = AssertionData.readConfiguration().filter { it.category == category }
+        val flickerService = FlickerService(assertions)
+
+        result.runs
+            .filter { it.assertionTag == AssertionTag.ALL }
+            .filter {
+                val hasWmTrace = it.wmSubject?.let { true } ?: false
+                val hasLayersTrace = it.layersSubject?.let { true } ?: false
+                hasWmTrace || hasLayersTrace
+            }
+            .forEach { run ->
+                val wmSubject = run.wmSubject as WindowManagerTraceSubject
+                val layersSubject = run.layersSubject as LayersTraceSubject
+
+                val outputDir = run.traceFiles
+                    .firstOrNull()
+                    ?.parent
+                    ?: error("Output dir not detected")
+
+                val wmTrace = wmSubject.trace
+                val layersTrace = layersSubject.trace
+                errors.add(flickerService.process(wmTrace, layersTrace, outputDir, category))
+            }
+
+        return errors
+    }
+
+    fun checkPresubmitAssertions() {
+        val errors = checkFlicker(AssertionConfigParser.PRESUBMIT_KEY)
+        failIfAnyError(errors)
+    }
+
+    fun checkPostsubmitAssertions() {
+        val errors = checkFlicker(AssertionConfigParser.POSTSUBMIT_KEY)
+        failIfAnyError(errors)
+    }
+
+    fun checkFlakyAssertions() {
+        val errors = checkFlicker(AssertionConfigParser.FLAKY_KEY)
+        failIfAnyError(errors)
+    }
+
+    private fun failIfAnyError(errors: List<ErrorTrace>) {
+        val errorMsg = errors.joinToString("\n") { runs ->
+            runs.entries.joinToString { state ->
+                state.errors.joinToString { "${it.assertionName}\n${it.message}" }
+            }
+        }
+        if (errorMsg.isNotEmpty()) {
+            error(errorMsg)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt
new file mode 100644
index 0000000..c18144e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import com.android.server.wm.flicker.service.assertors.AssertionData
+import com.android.server.wm.flicker.service.assertors.TransitionAssertor
+import com.android.server.wm.traces.common.errors.ErrorState
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.tags.TransitionTag
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Invokes the configured assertors and summarizes the results.
+ */
+class AssertionEngine(
+    private val assertions: List<AssertionData>,
+    private val logger: (String) -> Unit
+) {
+    private val knownTypes = assertions.map { it.transitionType }
+
+    fun analyze(
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace,
+        tagTrace: TagTrace
+    ): ErrorTrace {
+        val errors = mutableListOf<ErrorState>()
+        val allTransitions = getTransitionTags(tagTrace)
+
+        allTransitions
+            .filter { knownTypes.contains(it.tag.transition) }
+            .forEach { transition ->
+                val (filteredWmTrace, filteredLayersTrace) =
+                    splitTraces(transition, wmTrace, layersTrace)
+
+                val assertionsOfType = assertions
+                    .filter { it.transitionType == transition.tag.transition }
+                val assertor = TransitionAssertor(assertionsOfType, logger)
+                val errorTrace = assertor.analyze(
+                    transition.tag, filteredWmTrace, filteredLayersTrace)
+                errors.addAll(errorTrace)
+            }
+
+        /* Ensure all error states with same timestamp are merged */
+        val errorStates = errors.distinct()
+                .groupBy({ it.timestamp }, { it.errors.asList() })
+                .mapValues { (key, value) ->
+                    ErrorState(value.flatten().toTypedArray(), key.toString()) }
+                .values.toTypedArray()
+
+        return ErrorTrace(errorStates, source = "")
+    }
+
+    /**
+     * Extracts all [TransitionTag]s from a [TagTrace].
+     *
+     * @param tagTrace Tag Trace
+     * @return a list with [TransitionTag]
+     */
+    fun getTransitionTags(tagTrace: TagTrace): List<TransitionTag> {
+        return tagTrace.entries.flatMap { state ->
+            state.tags.filter { tag -> tag.isStartTag }
+                .map {
+                    TransitionTag(
+                        tag = it,
+                        startTimestamp = state.timestamp,
+                        endTimestamp = getEndTagTimestamp(tagTrace, it)
+                    )
+                }
+        }
+    }
+
+    private fun getEndTagTimestamp(tagTrace: TagTrace, tag: Tag): Long {
+        val finalTag = tag.copy(isStartTag = false)
+        return tagTrace.entries.firstOrNull { state -> state.tags.contains(finalTag) }?.timestamp
+            ?: throw RuntimeException("All open tags should be closed!")
+    }
+
+    /**
+     * Splits a [WindowManagerTrace] and a [LayersTrace] by a [Transition].
+     *
+     * @param tag a list with all [TransitionTag]s
+     * @param wmTrace Window Manager trace
+     * @param layersTrace Surface Flinger trace
+     * @return a list with [WindowManagerTrace] blocks
+     */
+    fun splitTraces(
+        tag: TransitionTag,
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace
+    ): Pair<WindowManagerTrace, LayersTrace> {
+        val filteredWmTrace = wmTrace.filter(tag.startTimestamp, tag.endTimestamp)
+        val filteredLayersTrace = layersTrace.filter(tag.startTimestamp, tag.endTimestamp)
+        return Pair(filteredWmTrace, filteredLayersTrace)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt
new file mode 100644
index 0000000..827b586
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import android.util.Log
+import com.android.server.wm.flicker.FLICKER_TAG
+import com.android.server.wm.flicker.monitor.TransitionMonitor.Companion.WINSCOPE_EXT
+import com.android.server.wm.flicker.service.assertors.AssertionData
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.service.TaggingEngine
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.errors.writeToFile
+import com.android.server.wm.traces.parser.tags.writeToFile
+import java.nio.file.Path
+
+/**
+ * Contains the logic for Flicker as a Service.
+ */
+class FlickerService @JvmOverloads constructor(
+    private val assertions: List<AssertionData> = AssertionData.readConfiguration()
+) {
+    /**
+     * The entry point for WM Flicker Service.
+     *
+     * Calls the Tagging Engine and the Assertion Engine.
+     *
+     * @param wmTrace Window Manager trace
+     * @param layersTrace Surface Flinger trace
+     * @return A list containing all failures
+     */
+    fun process(
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace,
+        outputDir: Path,
+        testTag: String
+    ): ErrorTrace {
+        val taggingEngine = TaggingEngine(wmTrace, layersTrace) { Log.v("$FLICKER_TAG-PROC", it) }
+        val tagTrace = taggingEngine.run()
+        val tagTraceFile = getFassFilePath(outputDir, testTag, "tag_trace")
+        tagTrace.writeToFile(tagTraceFile)
+
+        val assertionEngine = AssertionEngine(assertions) { Log.v("$FLICKER_TAG-ASSERT", it) }
+        val errorTrace = assertionEngine.analyze(wmTrace, layersTrace, tagTrace)
+        val errorTraceFile = getFassFilePath(outputDir, testTag, "error_trace")
+        errorTrace.writeToFile(errorTraceFile)
+        return errorTrace
+    }
+
+    companion object {
+        /**
+         * Returns the computed path for the Fass files.
+         *
+         * @param outputDir the output directory for the trace file
+         * @param testTag the tag to identify the test
+         * @param file the name of the trace file
+         * @return the path to the trace file
+         */
+        internal fun getFassFilePath(outputDir: Path, testTag: String, file: String): Path =
+                outputDir.resolve("${testTag}_$file$WINSCOPE_EXT")
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt
new file mode 100644
index 0000000..cc2d839
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import android.util.Log
+import com.android.server.wm.flicker.FLICKER_TAG
+import com.android.server.wm.traces.common.tags.Transition
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+object AssertionConfigParser {
+    private const val ASSERTORS_KEY = "assertors"
+    private const val CLASS_KEY = "class"
+    private const val ARGS_KEY = "args"
+    private const val TRANSITION_KEY = "transition"
+    private const val ASSERTIONS_KEY = "assertions"
+
+    internal const val PRESUBMIT_KEY = "presubmit"
+    internal const val POSTSUBMIT_KEY = "postsubmit"
+    internal const val FLAKY_KEY = "flaky"
+
+    /**
+     * Parses assertor config JSON file. The format expected is:
+     * <pre>
+     * {
+     *  "assertors": [
+     *   {
+     *      "transition": "ROTATION",
+     *      "assertions": {
+     *         "presubmit": [
+     *              "navBarWindowIsVisible"
+     *              "navBarLayerIsVisible",
+     *              "navBarLayerRotatesAndScales"
+     *          ],
+     *          "postsubmit": [ ],
+     *          "flaky": [
+     *              "entireScreenCovered"
+     *          ]
+     *      }
+     *   }
+     *  ]
+     * }
+     * </pre>
+     *
+     * @param config string containing a json file
+     * @return a list of [AssertionData] assertions
+     */
+    @JvmStatic
+    fun parseConfigFile(config: String): List<AssertionData> {
+        val assertorsConfig = mutableListOf<AssertionData>()
+        val jsonArray = JSONObject(config).getJSONArray(ASSERTORS_KEY)
+
+        for (i in 0 until jsonArray.length()) {
+            val jsonObject = jsonArray.getJSONObject(i)
+            val jsonAssertions = jsonObject.getJSONObject(ASSERTIONS_KEY)
+            val transitionType = Transition.valueOf(jsonObject.getString(TRANSITION_KEY))
+            val presubmit = parseAssertionArray(
+                jsonAssertions.getJSONArray(PRESUBMIT_KEY), transitionType, PRESUBMIT_KEY)
+            val postsubmit = parseAssertionArray(
+                jsonAssertions.getJSONArray(POSTSUBMIT_KEY), transitionType, POSTSUBMIT_KEY)
+            val flaky = parseAssertionArray(
+                jsonAssertions.getJSONArray(FLAKY_KEY), transitionType, FLAKY_KEY)
+            val assertionsList = presubmit + postsubmit + flaky
+
+            assertorsConfig.addAll(assertionsList)
+        }
+
+        return assertorsConfig
+    }
+
+    /**
+     * Splits an assertions JSONArray into an array of [AssertionData].
+     *
+     * @param assertionsArray a [JSONArray] with assertion names
+     * @param transitionType type of transition connected to this assertion
+     * @param category the category of the assertion (presubmit/postsubmit/flaky)
+     * @return an array of assertion details
+     */
+    @JvmStatic
+    private fun parseAssertionArray(
+        assertionsArray: JSONArray,
+        transitionType: Transition,
+        category: String
+    ): List<AssertionData> {
+        val assertions = mutableListOf<AssertionData>()
+        try {
+            for (i in 0 until assertionsArray.length()) {
+                val assertionObj = assertionsArray.getJSONObject(i)
+                val assertionClass = assertionObj.getString(CLASS_KEY)
+                val args = mutableListOf<String>()
+                if (assertionObj.has(ARGS_KEY)) {
+                    val assertionArgsArray = assertionObj.getJSONArray(ARGS_KEY)
+                    for (j in 0 until assertionArgsArray.length()) {
+                        val arg = assertionArgsArray.getString(j)
+                        args.add(arg)
+                    }
+                }
+                Log.v(FLICKER_TAG, "Creating assertion for class $assertionClass")
+                assertions.add(
+                    AssertionData.fromString(transitionType, category, assertionClass, args))
+            }
+        } catch (e: JSONException) {
+            throw RuntimeException(e)
+        }
+
+        return assertions
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt
new file mode 100644
index 0000000..768b867
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.flicker.service.FlickerService
+import com.android.server.wm.traces.common.tags.Transition
+import java.io.FileNotFoundException
+
+/**
+ * Stores data for FASS assertions.
+ */
+data class AssertionData(
+    val transitionType: Transition,
+    val assertion: BaseAssertion,
+    val category: String
+) {
+    companion object {
+        /**
+         * Returns the name of the assertors configuration file.
+         */
+        private const val CONFIG_FILE_NAME = "config.json"
+
+        /**
+         * Creates an assertion data based on it's fully-qualified class path [cls] and set
+         * its category to [category]
+         */
+        fun fromString(
+            transitionType: Transition,
+            category: String,
+            cls: String,
+            args: List<String>
+        ): AssertionData {
+            val clsDescriptor = Class.forName(cls)
+
+            val assertionObj = if (args.isEmpty()) {
+                clsDescriptor.newInstance() as BaseAssertion
+            } else {
+                val ctor = clsDescriptor.constructors
+                    .firstOrNull { it.parameterCount == args.size }
+                    ?: error("Constructor not found")
+                ctor.newInstance(*args.toTypedArray()) as BaseAssertion
+            }
+
+            return AssertionData(transitionType, assertionObj, category)
+        }
+
+        /**
+         * Reads the assertions configuration for the configuration file.
+         *
+         * @param fileName the location of the configuration file
+         * @return a list with assertors configuration
+         *
+         * @throws FileNotFoundException when there is no config file
+         */
+        @JvmOverloads
+        fun readConfiguration(fileName: String = CONFIG_FILE_NAME): List<AssertionData> {
+            val fileContent = FlickerService::class.java.classLoader.getResource(fileName)
+                ?.readText(Charsets.UTF_8)
+                ?: throw FileNotFoundException("A configuration file must exist!")
+            return AssertionConfigParser.parseConfigFile(fileContent)
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt
new file mode 100644
index 0000000..0815e03
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.layers.LayerSubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowStateSubject
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.TransitionTag
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+
+/**
+ * Base calss for a FASS assertion
+ */
+abstract class BaseAssertion {
+    private var failureSubject: FlickerSubject? = null
+
+    /**
+     * Assertion name
+     */
+    val name: String = this::class.java.simpleName
+
+    /**
+     * Run specific assertion evaluation block
+     *
+     * @param tag a list with all [TransitionTag]s
+     * @param wmSubject Window Manager trace subject
+     * @param layerSubject Surface Flinger trace subject
+     */
+    protected abstract fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    )
+
+    /**
+     * Evaluate the assertion on a transition [Tag] in a [WindowManagerTraceSubject] and
+     * [LayersTraceSubject]
+     *
+     * @param tag a list with all [TransitionTag]s
+     * @param wmSubject Window Manager trace subject
+     * @param layerSubject Surface Flinger trace subject
+     */
+    fun evaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        try {
+            doEvaluate(tag, wmSubject, layerSubject)
+        } catch (e: FlickerSubjectException) {
+            failureSubject = e.subject
+            throw e
+        }
+    }
+
+    /**
+     * Returns the layer responsible for the failure, if any
+     *
+     * @param tag a list with all [TransitionTag]s
+     * @param wmSubject Window Manager trace subject
+     * @param layerSubject Surface Flinger trace subject
+     */
+    open fun getFailureLayer(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ): Layer? {
+        val failureSubject = failureSubject
+        return if (failureSubject is LayerSubject) {
+            failureSubject.layer
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Returns the window responsible for the last failure, if any
+     *
+     * @param tag a list with all [TransitionTag]s
+     * @param wmSubject Window Manager trace subject
+     * @param layerSubject Surface Flinger trace subject
+     */
+    open fun getFailureWindow(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ): WindowState? {
+        val failureSubject = failureSubject
+        return if (failureSubject is WindowStateSubject) {
+            failureSubject.windowState
+        } else {
+            null
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt
new file mode 100644
index 0000000..c6d9a76
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.traces.common.FlickerComponentName
+
+object Components {
+    val LAUNCHER = FlickerComponentName("com.google.android.apps.nexuslauncher",
+        "com.google.android.apps.nexuslauncher.NexusLauncherActivity")
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt
new file mode 100644
index 0000000..9ec5a29
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import android.util.Log
+import com.android.server.wm.flicker.FLICKER_TAG
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.errors.Error
+import com.android.server.wm.traces.common.errors.ErrorState
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.service.ITransitionAssertor
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Class that runs FASS assertions.
+ */
+class TransitionAssertor(
+    private val assertions: List<AssertionData>,
+    private val logger: (String) -> Unit
+) : ITransitionAssertor {
+    /** {@inheritDoc} */
+    override fun analyze(
+        tag: Tag,
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace
+    ): ErrorTrace {
+        val errorStates = mutableMapOf<Long, MutableList<Error>>()
+
+        errorStates.putAll(
+            runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.PRESUBMIT_KEY))
+        errorStates.putAll(
+            runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.POSTSUBMIT_KEY))
+        errorStates.putAll(
+            runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.FLAKY_KEY))
+
+        return buildErrorTrace(errorStates)
+    }
+
+    private fun runCategoryAssertions(
+        tag: Tag,
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace,
+        categoryKey: String
+    ): Map<Long, MutableList<Error>> {
+        logger.invoke("Running assertions for $tag $categoryKey")
+        val wmSubject = WindowManagerTraceSubject.assertThat(wmTrace)
+        val layersSubject = LayersTraceSubject.assertThat(layersTrace)
+        val assertions = assertions.filter { it.category == categoryKey }
+        return runAssertionsOnSubjects(tag, wmSubject, layersSubject, assertions)
+    }
+
+    private fun runAssertionsOnSubjects(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject,
+        assertions: List<AssertionData>
+    ): Map<Long, MutableList<Error>> {
+        val errors = mutableMapOf<Long, MutableList<Error>>()
+
+        try {
+            assertions.forEach {
+                val assertion = it.assertion
+                logger.invoke("Running assertion $assertion")
+                val result = assertion.runCatching { evaluate(tag, wmSubject, layerSubject) }
+                if (result.isFailure) {
+                    val layer = assertion.getFailureLayer(tag, wmSubject, layerSubject)
+                    val window = assertion.getFailureWindow(tag, wmSubject, layerSubject)
+                    val exception = result.exceptionOrNull() as FlickerSubjectException
+
+                    errors.putIfAbsent(exception.timestamp, mutableListOf())
+                    val errorEntry = Error(
+                        stacktrace = exception.stackTraceToString(),
+                        message = exception.message,
+                        layerId = layer?.id ?: 0,
+                        windowToken = window?.token ?: "",
+                        assertionName = assertion.name
+                    )
+                    errors.getValue(exception.timestamp).add(errorEntry)
+                }
+            }
+        } catch (e: NoSuchMethodException) {
+            Log.e("$FLICKER_TAG-ASSERT", "Assertion method not found", e)
+        } catch (e: SecurityException) {
+            Log.e("$FLICKER_TAG-ASSERT", "Unable to get assertion method", e)
+        }
+
+        return errors
+    }
+
+    private fun buildErrorTrace(errors: MutableMap<Long, MutableList<Error>>): ErrorTrace {
+        val errorStates = errors.map { entry ->
+            val timestamp = entry.key
+            val stateTags = entry.value
+            ErrorState(stateTags.toTypedArray(), timestamp.toString())
+        }
+        return ErrorTrace(errorStates.toTypedArray(), source = "")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt
new file mode 100644
index 0000000..1fdce0c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+abstract class AppComponentBaseTest : BaseAssertion() {
+    protected fun getComponentName(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject
+    ): FlickerComponentName {
+        try {
+            val windowName = getWindowState(tag, wmSubject).name
+            return FlickerComponentName.unflattenFromString(windowName)
+        } catch (e: Throwable) {
+            throw FlickerSubjectException(wmSubject, e)
+        }
+    }
+
+    private fun getWindowState(tag: Tag, wmSubject: WindowManagerTraceSubject) =
+        wmSubject.subjects.last().windowState {
+            it.layerId == tag.layerId || it.token == tag.windowToken
+        }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt
new file mode 100644
index 0000000..58a4e20
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [getWindowState] layer is invisible at the end of the transition
+ */
+class AppLayerIsInvisibleAtEnd : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.last().isInvisible(getComponentName(tag, wmSubject))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt
new file mode 100644
index 0000000..2c40e57
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [getWindowState] layer is invisible at the start of the transition
+ */
+class AppLayerIsInvisibleAtStart : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.first().isInvisible(getComponentName(tag, wmSubject))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt
new file mode 100644
index 0000000..3670d26
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [getWindowState] layer is visible at the end of the transition
+ */
+class AppLayerIsVisibleAtEnd : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.last().isVisible(getComponentName(tag, wmSubject))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt
new file mode 100644
index 0000000..479c710
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [getWindowState] layer is visible at the start of the transition
+ */
+class AppLayerIsVisibleAtStart : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.first().isVisible(getComponentName(tag, wmSubject))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt
new file mode 100644
index 0000000..1591f7f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.Components
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Asserts that:
+ *     [Components.LAUNCHER] is visible at the start of the trace
+ *     [Components.LAUNCHER] becomes invisible during the trace and (in the same entry)
+ *     [getWindowState] becomes visible
+ *     [getWindowState] remains visible until the end of the trace
+ */
+class AppLayerReplacesLauncher : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.isVisible(Components.LAUNCHER)
+            .then()
+            .isVisible(getComponentName(tag, wmSubject))
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt
new file mode 100644
index 0000000..958327d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.Components
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that [Components.LAUNCHER] is the top visible app window at the start of the transition
+ * and that it is replaced by [getWindowState] during the transition
+ */
+class AppWindowReplacesLauncherAsTopWindow : AppComponentBaseTest() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isAppWindowOnTop(Components.LAUNCHER)
+            .then()
+            .isAppWindowOnTop(getComponentName(tag, wmSubject))
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt
new file mode 100644
index 0000000..30288ea
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.traces.common.FlickerComponentName
+
+abstract class ComponentBaseTest(windowName: String) : BaseAssertion() {
+    protected val component = FlickerComponentName.unflattenFromString(windowName)
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt
new file mode 100644
index 0000000..9820eaa
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the stack space of all displays is fully covered by any visible layer,
+ * during the whole transitions
+ */
+class EntireScreenCoveredAlways : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.invoke("entireScreenCovered") { entry ->
+            entry.entry.displays.forEach { display ->
+                entry.visibleRegion().coversAtLeast(display.layerStackSpace)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt
new file mode 100644
index 0000000..62b2539
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the stack space of all displays is fully covered by any visible layer,
+ * at the end of the transition
+ */
+class EntireScreenCoveredAtEnd : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val subject = layerSubject.last()
+        subject.entry.displays.forEach { display ->
+            subject.visibleRegion().coversAtLeast(display.layerStackSpace)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt
new file mode 100644
index 0000000..c51eb51
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the stack space of all displays is fully covered by any visible layer,
+ * at the start of the transition
+ */
+class EntireScreenCoveredAtStart : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val subject = layerSubject.first()
+        subject.entry.displays.forEach { display ->
+            subject.visibleRegion().coversAtLeast(display.layerStackSpace)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt
new file mode 100644
index 0000000..4025a95
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.Components
+
+/**
+ * Checks that [Components.LAUNCHER] starts on top and moves out of top during the transition
+ */
+class LauncherWindowMovesOutOfTop : WindowMovesOutOfTop(Components.LAUNCHER.toWindowName())
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt
new file mode 100644
index 0000000..8215101
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.Components
+
+/**
+ * Checks that [Components.LAUNCHER] starts not on top and moves to top during the transition
+ */
+class LauncherWindowMovesToTop : WindowMovesToTop(Components.LAUNCHER.toWindowName())
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt
new file mode 100644
index 0000000..a62de1d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.Components
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that [getWindowState] is the top visible app window at the start of the transition and
+ * that it is replaced by [Components.LAUNCHER] during the transition
+ */
+class LauncherWindowReplacesAppAsTopWindow : AppComponentBaseTest() {
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isAppWindowOnTop(getComponentName(tag, wmSubject))
+            .then()
+            .isAppWindowOnTop(Components.LAUNCHER)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt
new file mode 100644
index 0000000..150c2a9
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is invisible during the entire transition
+ */
+class LayerIsInvisibleAlways(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.isVisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt
new file mode 100644
index 0000000..198302b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is invisible at the end of the transition
+ */
+class LayerIsInvisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.last().isInvisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt
new file mode 100644
index 0000000..e241b7e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is invisible at the start of the transition
+ */
+class LayerIsInvisibleAtStart(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.first().isInvisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt
new file mode 100644
index 0000000..a4c1265
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is visible during the entire transition
+ */
+class LayerIsVisibleAlways(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.isVisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt
new file mode 100644
index 0000000..5d012ce
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is visible at the end of the transition
+ */
+class LayerIsVisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.last().isVisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt
new file mode 100644
index 0000000..0d61d87
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] layer is visible at the start of the transition
+ */
+class LayerIsVisibleAtStart(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.first().isVisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt
new file mode 100644
index 0000000..4ab5819
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [FlickerComponentName.NAV_BAR] layer is placed at the correct position at the
+ * end of the transition
+ */
+class NavBarLayerPositionAtEnd : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val lastLayersSubject = layerSubject.last()
+        val display = lastLayersSubject.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        lastLayersSubject.visibleRegion(FlickerComponentName.NAV_BAR)
+            .coversExactly(WindowUtils.getNavigationBarPosition(display))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt
new file mode 100644
index 0000000..eba2fbe
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [FlickerComponentName.NAV_BAR] layer is placed at the correct position at the
+ * start of the transition
+ */
+class NavBarLayerPositionAtStart : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val firstLayersSubject = layerSubject.first()
+        val display = firstLayersSubject.entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("There is no display!")
+        firstLayersSubject.visibleRegion(FlickerComponentName.NAV_BAR)
+            .coversExactly(WindowUtils.getNavigationBarPosition(display))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt
new file mode 100644
index 0000000..3ec9cc7
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+open class NonAppWindowBecomesVisible(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isNonAppWindowInvisible(component)
+            .then()
+            .isAppWindowVisible(component)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt
new file mode 100644
index 0000000..2cbc239
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] window is invisible during the entire transition
+ */
+class NonAppWindowIsInvisibleAlways(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isNonAppWindowInvisible(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt
new file mode 100644
index 0000000..6492193
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] window is visible during the entire transition
+ */
+class NonAppWindowIsVisibleAlways(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isNonAppWindowVisible(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt
new file mode 100644
index 0000000..7471bc3
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] window is visible at the end of the transition
+ */
+class NonAppWindowIsVisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.last().isNonAppWindowVisible(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt
new file mode 100644
index 0000000..c8f98c0
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [component] window is visible at the end of the transition
+ */
+class NonAppWindowIsVisibleAtStart(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.first().isNonAppWindowVisible(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt
new file mode 100644
index 0000000..1f1c1c7
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that the [FlickerComponentName.ROTATION] layer appears during the transition,
+ * doesn't flicker, and disappears before the transition is complete.
+ */
+class RotationLayerAppearsAndVanishes : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val window = wmSubject.trace.entries.first().topVisibleAppWindow
+        val appComponent = FlickerComponentName.unflattenFromString(window)
+        layerSubject.isVisible(appComponent)
+            .then()
+            .isVisible(FlickerComponentName.ROTATION)
+            .then()
+            .isVisible(appComponent)
+            .isInvisible(FlickerComponentName.ROTATION)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt
new file mode 100644
index 0000000..d26d1c6
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [FlickerComponentName.STATUS_BAR] layer is placed at the correct position at the
+ * end of the transition
+ */
+class StatusBarLayerPositionAtEnd : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val endDisplay = layerSubject.last().entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("Display not found")
+
+        layerSubject.last().visibleRegion(FlickerComponentName.STATUS_BAR)
+            .coversExactly(WindowUtils.getStatusBarPosition(endDisplay))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt
new file mode 100644
index 0000000..ad89b6a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks if the [FlickerComponentName.STATUS_BAR] layer is placed at the correct position at the
+ * start of the transition
+ */
+class StatusBarLayerPositionAtStart : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        val startDisplay = layerSubject.first().entry.displays.minByOrNull { it.id }
+            ?: throw RuntimeException("Display not found")
+
+        layerSubject.first().visibleRegion(FlickerComponentName.STATUS_BAR)
+            .coversExactly(WindowUtils.getStatusBarPosition(startDisplay))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt
new file mode 100644
index 0000000..57b39d9
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that all layers that are visible on the trace, are visible for at least 2
+ * consecutive entries.
+ */
+class VisibleLayersShownMoreThanOneConsecutiveEntry : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        layerSubject.visibleLayersShownMoreThanOneConsecutiveEntry()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt
new file mode 100644
index 0000000..c029479
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.service.assertors.BaseAssertion
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that all windows that are visible on the trace, are visible for at least 2
+ * consecutive entries.
+ */
+class VisibleWindowsShownMoreThanOneConsecutiveEntry : BaseAssertion() {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.visibleWindowsShownMoreThanOneConsecutiveEntry()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt
new file mode 100644
index 0000000..b717e3f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that [component] starts on top and moves out of top during the transition
+ */
+open class WindowMovesOutOfTop(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isAppWindowOnTop(component)
+            .then()
+            .isAppWindowNotOnTop(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt
new file mode 100644
index 0000000..baf5ef2
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors.common
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.tags.Tag
+
+/**
+ * Checks that [component] starts not on top and moves to top during the transition
+ */
+open class WindowMovesToTop(windowName: String) : ComponentBaseTest(windowName) {
+    /** {@inheritDoc} */
+    override fun doEvaluate(
+        tag: Tag,
+        wmSubject: WindowManagerTraceSubject,
+        layerSubject: LayersTraceSubject
+    ) {
+        wmSubject.isAppWindowNotOnTop(component)
+            .then()
+            .isAppWindowOnTop(component)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json b/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json
new file mode 100644
index 0000000..4768e45
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json
@@ -0,0 +1,162 @@
+{
+  "assertors": [
+    {
+      "transition": "ROTATION",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.RotationLayerAppearsAndVanishes"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry"
+          }
+        ]
+      }
+    },
+    {
+      "transition": "APP_LAUNCH",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerReplacesLauncher"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAtEnd",
+            "args": [
+              "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsInvisibleAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsVisibleAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppWindowReplacesLauncherAsTopWindow"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LauncherWindowMovesOutOfTop"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.StatusBarLayerPositionAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.StatusBarLayerPositionAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry"
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
index 7a5ea0c..4f775ff 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
@@ -17,13 +17,47 @@
 package com.android.server.wm.flicker.traces
 
 import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.traces.common.prettyTimestamp
 
 /**
  * Exception thrown by [FlickerSubject]s
  */
 class FlickerSubjectException(
-    flickerSubject: FlickerSubject,
+    internal val subject: FlickerSubject,
     cause: Throwable
-) : AssertionError(flickerSubject.defaultFacts, cause) {
-    internal val facts = flickerSubject.defaultFacts
+) : AssertionError(cause.message, if (cause is FlickerSubjectException) null else cause) {
+    internal val timestamp = subject.timestamp
+    private val prettyTimestamp =
+            if (timestamp > 0) "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" else ""
+
+    internal val errorType: String =
+            if (cause is AssertionError) "Flicker assertion error" else "Unknown error"
+
+    internal val errorDescription = buildString {
+        appendln("Where? $prettyTimestamp")
+        val message = (cause.message ?: "").split(("\n"))
+        append("What? ")
+        if (message.size == 1) {
+            // Single line error message
+            appendln(message.first())
+        } else {
+            // Multi line error message
+            appendln()
+            message.forEach { appendln("\t$it") }
+        }
+    }
+
+    internal val subjectInformation = buildString {
+        appendln("Facts:")
+        subject.completeFacts.forEach { append("\t").appendln(it) }
+    }
+
+    override val message: String
+        get() = buildString {
+            appendln(errorType)
+            appendln()
+            append(errorDescription)
+            appendln()
+            append(subjectInformation)
+        }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt
index b6d2607..b5ad419 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt
@@ -19,6 +19,8 @@
 import com.android.server.wm.flicker.assertions.Assertion
 import com.android.server.wm.flicker.assertions.AssertionsChecker
 import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.traces.common.prettyTimestamp
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 
 /**
@@ -28,16 +30,37 @@
     fm: FailureMetadata,
     data: Any?
 ) : FlickerSubject(fm, data) {
+    override val timestamp: Long get() = subjects.first().timestamp
+    override val selfFacts by lazy {
+        val firstTimestamp = subjects.firstOrNull()?.timestamp ?: 0L
+        val lastTimestamp = subjects.lastOrNull()?.timestamp ?: 0L
+        val first = "${prettyTimestamp(firstTimestamp)} (timestamp=$firstTimestamp)"
+        val last = "${prettyTimestamp(lastTimestamp)} (timestamp=$lastTimestamp)"
+        listOf(Fact.fact("Trace start", first),
+                Fact.fact("Trace end", last))
+    }
+
     protected val assertionsChecker = AssertionsChecker<EntrySubject>()
     private var newAssertionBlock = true
 
     abstract val subjects: List<EntrySubject>
 
-    protected fun addAssertion(name: String, assertion: Assertion<EntrySubject>) {
+    /**
+     * Adds a new assertion block (if preceded by [then]) or appends an assertion to the
+     * latest existing assertion block
+     *
+     * @param name Assertion name
+     * @param isOptional If this assertion is optional or must pass
+     */
+    protected fun addAssertion(
+        name: String,
+        isOptional: Boolean = false,
+        assertion: Assertion<EntrySubject>
+    ) {
         if (newAssertionBlock) {
-            assertionsChecker.add(name, assertion)
+            assertionsChecker.add(name, isOptional, assertion)
         } else {
-            assertionsChecker.append(name, assertion)
+            assertionsChecker.append(name, isOptional, assertion)
         }
         newAssertionBlock = false
     }
@@ -68,7 +91,30 @@
      * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
      * after checkA passes.
      */
-    protected fun startAssertionBlock() {
+    open fun then(): FlickerTraceSubject<EntrySubject> = apply {
+        startAssertionBlock()
+    }
+
+    /**
+     * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+     * end of the trace without passing any assertion, return a failure with the name/reason from
+     * the first assertion
+     *
+     * @return
+     */
+    open fun skipUntilFirstAssertion(): FlickerTraceSubject<EntrySubject> =
+        apply { assertionsChecker.skipUntilFirstAssertion() }
+
+    /**
+     * Signal that the last assertion set is complete. The next assertion added will start a new
+     * set of assertions.
+     *
+     * E.g.: checkA().then().checkB()
+     *
+     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
+     * after checkA passes.
+     */
+    private fun startAssertionBlock() {
         newAssertionBlock = true
     }
 
@@ -76,11 +122,14 @@
      * Checks whether all the trace entries on the list are visible for more than one consecutive
      * entry
      *
-     * @param [visibleEntries] a list of all the entries with their name and index
+     * @param [visibleEntriesProvider] a list of all the entries with their name and index
      */
     protected fun visibleEntriesShownMoreThanOneConsecutiveTime(
         visibleEntriesProvider: (EntrySubject) -> Set<String>
     ) {
+        if (subjects.isEmpty()) {
+            return
+        }
         var lastVisible = visibleEntriesProvider(subjects.first())
         val lastNew = lastVisible.toMutableSet()
 
@@ -102,4 +151,7 @@
             lastEntry.fail("$lastNew is not visible for 2 entries")
         }
     }
+
+    override fun toString(): String = "${this::class.simpleName}" +
+            "(${subjects.firstOrNull()?.timestamp ?: 0},${subjects.lastOrNull()?.timestamp ?: 0})"
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt
index 6c3f81b..d10a9f1 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt
@@ -33,9 +33,10 @@
  */
 class RegionSubject(
     fm: FailureMetadata,
-    private val subjects: List<FlickerSubject>,
+    override val parent: FlickerSubject?,
     val region: android.graphics.Region
 ) : FlickerSubject(fm, region) {
+    override val timestamp: Long get() = parent?.timestamp ?: 0
     private val topPositionSubject
         get() = check(MSG_ERROR_TOP_POSITION).that(region.bounds.top)
     private val bottomPositionSubject
@@ -50,15 +51,13 @@
     private val android.graphics.Rect.area get() = this.width() * this.height()
     private val Rect.area get() = this.width * this.height
 
-    override val defaultFacts: String = buildString {
-        subjects.forEach { subject -> appendln(subject.defaultFacts) }
-    }
+    override val selfFacts = listOf(Fact.fact("Region - Covered", region.toString()))
 
     /**
      * {@inheritDoc}
      */
     override fun clone(): FlickerSubject {
-        return RegionSubject(fm, subjects, region)
+        return RegionSubject(fm, parent, region)
     }
 
     /**
@@ -76,6 +75,34 @@
     }
 
     /**
+     * Subtracts [other] from this subject [region]
+     */
+    fun minus(other: Region): RegionSubject = minus(other.toAndroidRegion())
+
+    /**
+     * Subtracts [other] from this subject [region]
+     */
+    fun minus(other: android.graphics.Region): RegionSubject {
+        val remainingRegion = android.graphics.Region(this.region)
+        remainingRegion.op(other, android.graphics.Region.Op.XOR)
+        return assertThat(remainingRegion, this)
+    }
+
+    /**
+     * Adds [other] to this subject [region]
+     */
+    fun plus(other: Region): RegionSubject = plus(other.toAndroidRegion())
+
+    /**
+     * Adds [other] to this subject [region]
+     */
+    fun plus(other: android.graphics.Region): RegionSubject {
+        val remainingRegion = android.graphics.Region(this.region)
+        remainingRegion.op(other, android.graphics.Region.Op.UNION)
+        return assertThat(remainingRegion, this)
+    }
+
+    /**
      * Asserts that the top and bottom coordinates of [RegionSubject.region] are smaller
      * or equal to those of [region].
      *
@@ -503,26 +530,24 @@
          * Boiler-plate Subject.Factory for RectSubject
          */
         @JvmStatic
-        @JvmOverloads
         fun getFactory(
-            flickerSubjects: List<FlickerSubject> = emptyList()
+            parent: FlickerSubject?
         ) = Factory { fm: FailureMetadata, region: android.graphics.Region? ->
             val subjectRegion = region ?: android.graphics.Region()
-            RegionSubject(fm, flickerSubjects, subjectRegion)
+            RegionSubject(fm, parent, subjectRegion)
         }
 
         /**
          * User-defined entry point for existing android regions
          */
         @JvmStatic
-        @JvmOverloads
         fun assertThat(
             region: android.graphics.Region?,
-            flickerSubjects: List<FlickerSubject> = emptyList()
+            parent: FlickerSubject? = null
         ): RegionSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(flickerSubjects))
+                .about(getFactory(parent))
                 .that(region ?: android.graphics.Region()) as RegionSubject
             strategy.init(subject)
             return subject
@@ -533,59 +558,47 @@
          */
         @JvmStatic
         @JvmOverloads
-        fun assertThat(
-            rect: Array<Rect>,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(Region(rect), flickerSubjects)
+        fun assertThat(rect: Array<Rect>, parent: FlickerSubject? = null): RegionSubject =
+                assertThat(Region(rect), parent)
 
         /**
          * User-defined entry point for existing rects
          */
         @JvmStatic
         @JvmOverloads
-        fun assertThat(
-            rect: Rect?,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(Region(rect), flickerSubjects)
+        fun assertThat(rect: Rect?, parent: FlickerSubject? = null): RegionSubject =
+                assertThat(Region(rect), parent)
 
         /**
          * User-defined entry point for existing rects
          */
         @JvmStatic
-        fun assertThat(
-            rect: RectF?,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(rect?.toRect(), flickerSubjects)
+        @JvmOverloads
+        fun assertThat(rect: RectF?, parent: FlickerSubject? = null): RegionSubject =
+                assertThat(rect?.toRect(), parent)
 
         /**
          * User-defined entry point for existing rects
          */
         @JvmStatic
-        fun assertThat(
-            rect: Array<RectF>,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(
-            mergeRegions(rect.map { Region(it.toRect()) }.toTypedArray()),
-            flickerSubjects)
+        @JvmOverloads
+        fun assertThat(rect: Array<RectF>, parent: FlickerSubject? = null): RegionSubject =
+            assertThat(mergeRegions(rect.map { Region(it.toRect()) }.toTypedArray()), parent)
 
         /**
          * User-defined entry point for existing regions
          */
         @JvmStatic
         @JvmOverloads
-        fun assertThat(
-            regions: Array<Region>,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(mergeRegions(regions), flickerSubjects)
+        fun assertThat(regions: Array<Region>, parent: FlickerSubject? = null): RegionSubject =
+                assertThat(mergeRegions(regions), parent)
 
         /**
          * User-defined entry point for existing regions
          */
         @JvmStatic
         @JvmOverloads
-        fun assertThat(
-            region: Region?,
-            flickerSubjects: List<FlickerSubject> = emptyList()
-        ): RegionSubject = assertThat(region?.toAndroidRegion(), flickerSubjects)
+        fun assertThat(region: Region?, parent: FlickerSubject? = null): RegionSubject =
+                assertThat(region?.toAndroidRegion(), parent)
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt
index 704e46b..8036730 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt
@@ -18,6 +18,7 @@
 
 import com.android.server.wm.flicker.assertions.AssertionsChecker
 import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.traces.common.prettyTimestamp
 import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.Subject.Factory
@@ -30,10 +31,15 @@
     failureMetadata: FailureMetadata,
     private val trace: List<FocusEvent>
 ) : FlickerSubject(failureMetadata, trace) {
-    override val defaultFacts: String by lazy {
-        val first = subjects.first().defaultFacts
-        val last = subjects.last().defaultFacts
-        "EventLogSubject($first, $last)"
+    override val timestamp: Long get() = 0
+    override val parent: FlickerSubject? get() = null
+    override val selfFacts by lazy {
+        val firstTimestamp = subjects.first().timestamp
+        val lastTimestamp = subjects.last().timestamp
+        val first = "${prettyTimestamp(firstTimestamp)} (timestamp=$firstTimestamp)"
+        val last = "${prettyTimestamp(lastTimestamp)} (timestamp=$lastTimestamp)"
+        listOf(Fact.fact("Trace start", first),
+                Fact.fact("Trace end", last))
     }
 
     /** {@inheritDoc} */
@@ -52,7 +58,7 @@
         focusList + trace.filter { it.hasFocus() }.map { it.window }
     }
 
-    fun focusChanges(windows: Array<out String>) = apply {
+    fun focusChanges(vararg windows: String) = apply {
         if (windows.isNotEmpty()) {
             val focusChanges = _focusChanges
                 .dropWhile { !it.contains(windows.first()) }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt
index fb6745d..1f691eb 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt
@@ -18,19 +18,21 @@
 
 import com.android.server.wm.flicker.assertions.FlickerSubject
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.StandardSubjectBuilder
 
 class FocusEventSubject(
     fm: FailureMetadata,
     val event: FocusEvent,
-    val trace: EventLogSubject?
+    override val parent: EventLogSubject?
 ) : FlickerSubject(fm, event) {
-    override val defaultFacts by lazy { event.toString() }
+    override val timestamp: Long get() = 0
+    override val selfFacts by lazy { listOf(Fact.simpleFact(event.toString())) }
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return FocusEventSubject(fm, event, trace)
+        return FocusEventSubject(fm, event, parent)
     }
 
     fun hasFocus() {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt
index efa0352..614cd9a 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt
@@ -20,8 +20,9 @@
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.assertions.FlickerSubject
 import com.android.server.wm.flicker.traces.RegionSubject
-import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.Size
 import com.android.server.wm.traces.common.layers.Layer
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.FailureStrategy
 import com.google.common.truth.StandardSubjectBuilder
@@ -48,8 +49,9 @@
  */
 class LayerSubject private constructor(
     fm: FailureMetadata,
+    override val parent: FlickerSubject,
+    override val timestamp: Long,
     val layer: Layer?,
-    val entry: LayerTraceEntrySubject?,
     private val layerName: String? = null
 ) : FlickerSubject(fm, layer) {
     val isEmpty: Boolean get() = layer == null
@@ -62,16 +64,19 @@
      * Visible region calculated by the Composition Engine
      */
     val visibleRegion: RegionSubject get() =
-        RegionSubject.assertThat(layer?.visibleRegion, listOf(this))
+        RegionSubject.assertThat(layer?.visibleRegion, this)
     /**
      * Visible region calculated by the Composition Engine (when available) or calculated
      * based on the layer bounds and transform
      */
     val screenBounds: RegionSubject get() =
-        RegionSubject.assertThat(layer?.screenBounds, listOf(this))
+        RegionSubject.assertThat(layer?.screenBounds, this)
 
-    override val defaultFacts: String =
-        "${entry?.defaultFacts ?: ""}\nFrame: ${layer?.currFrame}\nLayer: ${layer?.name}"
+    override val selfFacts = if (layer != null) {
+        listOf(Fact.fact("Frame", layer.currFrame), Fact.fact("Layer", layer.name))
+    } else {
+        listOf(Fact.fact("Layer name", layerName))
+    }
 
     /**
      * If the [layer] exists, executes a custom [assertion] on the current subject
@@ -83,7 +88,7 @@
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return LayerSubject(fm, layer, entry, layerName)
+        return LayerSubject(fm, parent, timestamp, layer, layerName)
     }
 
     /**
@@ -102,7 +107,7 @@
 
     @Deprecated("Prefer hasBufferSize(bounds)")
     fun hasBufferSize(size: Point): LayerSubject = apply {
-        val bounds = Bounds(size.x, size.y)
+        val bounds = Size(size.x, size.y)
         hasBufferSize(bounds)
     }
 
@@ -112,9 +117,9 @@
      *
      * @param size expected buffer size
      */
-    fun hasBufferSize(size: Bounds): LayerSubject = apply {
+    fun hasBufferSize(size: Size): LayerSubject = apply {
         layer ?: return exists()
-        val bufferSize = layer.activeBuffer?.size ?: Bounds.EMPTY
+        val bufferSize = Size(layer.activeBuffer.width, layer.activeBuffer.height)
         check("Incorrect buffer size").that(bufferSize).isEqualTo(size)
     }
 
@@ -162,50 +167,43 @@
          * Boiler-plate Subject.Factory for LayerSubject
          */
         @JvmStatic
-        @JvmOverloads
-        fun getFactory(entry: LayerTraceEntrySubject? = null) =
-            Factory { fm: FailureMetadata, subject: Layer? -> LayerSubject(fm, subject, entry) }
+        fun getFactory(parent: FlickerSubject, timestamp: Long, name: String?) =
+            Factory { fm: FailureMetadata, subject: Layer? ->
+                LayerSubject(fm, parent, timestamp, subject, name)
+            }
 
         /**
-         * User-defined entry point for existing layers
+         * User-defined parent point for existing layers
          */
         @JvmStatic
-        @JvmOverloads
         fun assertThat(
             layer: Layer?,
-            entry: LayerTraceEntrySubject? = null
+            parent: FlickerSubject,
+            timestamp: Long
         ): LayerSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(entry))
+                .about(getFactory(parent, timestamp, name = null))
                 .that(layer) as LayerSubject
             strategy.init(subject)
             return subject
         }
 
         /**
-         * User-defined entry point for non existing layers
+         * User-defined parent point for non existing layers
          */
         @JvmStatic
         internal fun assertThat(
             name: String,
-            entry: LayerTraceEntrySubject?
+            parent: FlickerSubject,
+            timestamp: Long
         ): LayerSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(entry, name))
+                .about(getFactory(parent, timestamp, name))
                 .that(null) as LayerSubject
             strategy.init(subject)
             return subject
         }
-
-        /**
-         * Boiler-plate Subject.Factory for LayerSubject
-         */
-        @JvmStatic
-        internal fun getFactory(entry: LayerTraceEntrySubject?, name: String) =
-            Factory { fm: FailureMetadata, subject: Layer? ->
-                LayerSubject(fm, subject, entry, name)
-            }
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt
index 7a902ae..8f35477 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt
@@ -18,12 +18,13 @@
 
 import com.android.server.wm.flicker.assertions.Assertion
 import com.android.server.wm.flicker.assertions.FlickerSubject
-import com.android.server.wm.flicker.containsAny
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.FlickerSubjectException
 import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.layers.Layer
 import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.LayersTrace
 import com.google.common.truth.ExpectFailure
 import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
@@ -56,12 +57,14 @@
 class LayerTraceEntrySubject private constructor(
     fm: FailureMetadata,
     val entry: LayerTraceEntry,
-    val trace: LayersTraceSubject?
+    val trace: LayersTrace?,
+    override val parent: FlickerSubject?
 ) : FlickerSubject(fm, entry) {
-    override val defaultFacts: String = "${trace?.defaultFacts ?: ""}\nEntry: $entry"
+    override val timestamp: Long get() = entry.timestamp
+    override val selfFacts = listOf(Fact.fact("Entry", entry))
 
     val subjects by lazy {
-        entry.flattenedLayers.map { LayerSubject.assertThat(it, this) }
+        entry.flattenedLayers.map { LayerSubject.assertThat(it, this, timestamp) }
     }
 
     /**
@@ -73,15 +76,7 @@
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return LayerTraceEntrySubject(fm, entry, trace)
-    }
-
-    /**
-     * Asserts that current entry subject has an [LayerTraceEntry.timestamp] equals to
-     * [timestamp]
-     */
-    fun hasTimestamp(timestamp: Long): LayerTraceEntrySubject = apply {
-        check("Wrong  entry timestamp").that(entry.timestamp).isEqualTo(timestamp)
+        return LayerTraceEntrySubject(fm, entry, trace, parent)
     }
 
     /**
@@ -103,112 +98,133 @@
     }
 
     /**
-     * Asserts that the current SurfaceFlinger state has [numberLayers] layers
-     */
-    fun hasLayersSize(numberLayers: Int): LayerTraceEntrySubject = apply {
-        check("Wrong number of layers in entry")
-            .that(entry.flattenedLayers.size)
-            .isEqualTo(numberLayers)
-    }
-
-    /**
-     * Obtains the region occupied by all layers with name containing any of [partialLayerNames]
+     * Obtains the region occupied by all layers with name containing [component]
      *
-     * @param partialLayerNames Name of the layer to search
+     * @param component Component to search
      * @param useCompositionEngineRegionOnly If true, uses only the region calculated from the
      *   Composition Engine (CE) -- visibleRegion in the proto definition. Otherwise calculates
      *   the visible region when the information is not available from the CE
      */
     fun visibleRegion(
-        vararg partialLayerNames: String,
+        component: FlickerComponentName? = null,
         useCompositionEngineRegionOnly: Boolean = true
     ): RegionSubject {
+        val layerName = component?.toLayerName() ?: ""
         val selectedLayers = subjects
-            .filter { it.name.containsAny(*partialLayerNames) }
+            .filter { it.name.contains(layerName) }
 
         if (selectedLayers.isEmpty()) {
-            fail("Could not find", partialLayerNames.joinToString(", "))
+            fail(listOf(
+                Fact.fact(ASSERTION_TAG, "visibleRegion(${component?.toLayerName() ?: "<any>"})"),
+                Fact.fact("Use composition engine region", useCompositionEngineRegionOnly),
+                Fact.fact("Could not find", layerName))
+            )
         }
 
         val visibleLayers = selectedLayers.filter { it.isVisible }
         return if (useCompositionEngineRegionOnly) {
             val visibleAreas = visibleLayers.mapNotNull { it.layer?.visibleRegion }.toTypedArray()
-            RegionSubject.assertThat(visibleAreas, selectedLayers)
+            RegionSubject.assertThat(visibleAreas, this)
         } else {
             val visibleAreas = visibleLayers.mapNotNull { it.layer?.screenBounds }.toTypedArray()
-            RegionSubject.assertThat(visibleAreas, selectedLayers)
+            RegionSubject.assertThat(visibleAreas, this)
         }
     }
 
     /**
-     * Asserts that the SurfaceFlinger state contains a [Layer] with [Layer.name] containing any of
-     * [partialLayerNames].
+     * Asserts the state contains a [Layer] with [Layer.name] containing [component].
      *
-     * @param partialLayerNames Name of the layers to search
+     * @param component Name of the layers to search
      */
-    fun contains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
-        val found = entry.flattenedLayers.any { it.name.containsAny(*partialLayerNames) }
-        if (partialLayerNames.isNotEmpty() && !found) {
-            fail("Could not find", partialLayerNames.joinToString(", "))
-        }
-    }
-
-    /**
-     * Asserts that the SurfaceFlinger state doesn't contain a [Layer] with [Layer.name] containing any of
-     *
-     * @param partialLayerNames Name of the layers to search
-     */
-    fun notContains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
-        val found = entry.flattenedLayers.none { it.name.containsAny(*partialLayerNames) }
+    fun contains(component: FlickerComponentName): LayerTraceEntrySubject = apply {
+        val layerName = component.toLayerName()
+        val found = entry.flattenedLayers.any { it.name.contains(layerName) }
         if (!found) {
-            fail("Could find", partialLayerNames)
+            fail(Fact.fact(ASSERTION_TAG, "contains(${component.toLayerName()})"),
+                Fact.fact("Could not find", layerName))
         }
     }
 
     /**
-     * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] is visible.
+     * Asserts the state doesn't contain a [Layer] with [Layer.name] containing any of
      *
-     * @param partialLayerNames Name of the layers to search
+     * @param component Name of the layers to search
      */
-    fun isVisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
-        contains(*partialLayerNames)
+    fun notContains(component: FlickerComponentName): LayerTraceEntrySubject = apply {
+        val layerName = component.toLayerName()
+        val foundEntry = subjects.firstOrNull { it.name.contains(layerName) }
+        foundEntry?.fail(Fact.fact(ASSERTION_TAG, "notContains(${component.toLayerName()})"),
+            Fact.fact("Could find", foundEntry))
+    }
+
+    /**
+     * Asserts that a [Layer] with [Layer.name] containing [component] is visible.
+     *
+     * @param component Name of the layers to search
+     */
+    fun isVisible(component: FlickerComponentName): LayerTraceEntrySubject = apply {
+        contains(component)
+        var target: FlickerSubject? = null
         var reason: Fact? = null
-        val filteredLayers = entry.flattenedLayers
-            .filter { it.name.containsAny(*partialLayerNames) }
+        val layerName = component.toLayerName()
+        val filteredLayers = subjects
+            .filter { it.name.contains(layerName) }
         for (layer in filteredLayers) {
-            if (layer.isHiddenByParent) {
-                reason = Fact.fact("Hidden by parent", layer.parent.name)
+            if (layer.layer?.isHiddenByParent == true) {
+                reason = Fact.fact("Hidden by parent", layer.layer.parent?.name)
+                target = layer
                 continue
             }
             if (layer.isInvisible) {
-                reason = Fact.fact("Is Invisible", layer.visibilityReason)
+                reason = Fact.fact("Is Invisible", layer.layer?.visibilityReason)
+                target = layer
                 continue
             }
             reason = null
+            target = null
             break
         }
 
-        if (reason != null) {
-            fail(reason)
+        reason?.run {
+            target?.fail(Fact.fact(ASSERTION_TAG, "isVisible(${component.toLayerName()})"), reason)
         }
     }
 
     /**
-     * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] doesn't exist or
+     * Asserts that a [Layer] with [Layer.name] containing [component] doesn't exist or
      * is invisible.
      *
-     * @param partialLayerNames Name of the layers to search
+     * @param component Name of the layers to search
      */
-    fun isInvisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
+    fun isInvisible(component: FlickerComponentName): LayerTraceEntrySubject = apply {
         try {
-            isVisible(*partialLayerNames)
+            isVisible(component)
         } catch (e: FlickerSubjectException) {
             val cause = e.cause
             require(cause is AssertionError)
             ExpectFailure.assertThat(cause).factKeys().isNotEmpty()
             return@apply
         }
-        fail("Layer is visible", partialLayerNames)
+        val layerName = component.toLayerName()
+        val foundEntry = subjects
+                .firstOrNull { it.name.contains(layerName) && it.isVisible }
+        foundEntry?.fail(Fact.fact(ASSERTION_TAG, "isInvisible(${component.toLayerName()})"),
+            Fact.fact("Is visible", foundEntry))
+    }
+
+    /**
+     * Obtains a [LayerSubject] for the first occurrence of a [Layer] with [Layer.name]
+     * containing [component].
+     * Always returns a subject, event when the layer doesn't exist. To verify if layer
+     * actually exists in the hierarchy use [LayerSubject.exists] or [LayerSubject.doesNotExist]
+     *
+     * @return LayerSubject that can be used to make assertions on a single layer matching
+     */
+    fun layer(component: FlickerComponentName): LayerSubject {
+        val name = component.toLayerName()
+        return layer {
+            it.name.contains(name)
+        }
     }
 
     /**
@@ -222,10 +238,27 @@
      * [name] and [frameNumber].
      */
     fun layer(name: String, frameNumber: Long): LayerSubject {
+        return layer(name) {
+            it.name.contains(name) && it.currFrame == frameNumber
+        }
+    }
+
+    /**
+     * Obtains a [LayerSubject] for the first occurrence of a [Layer] matching [predicate]
+     *
+     * Always returns a subject, event when the layer doesn't exist. To verify if layer
+     * actually exists in the hierarchy use [LayerSubject.exists] or [LayerSubject.doesNotExist]
+     *
+     * @param predicate to search for a layer
+     * @param name Name of the subject to use when not found (optional)
+     *
+     * @return [LayerSubject] that can be used to make assertions
+     */
+    @JvmOverloads
+    fun layer(name: String = "", predicate: (Layer) -> Boolean): LayerSubject {
         return subjects.firstOrNull {
-            it.layer?.name?.contains(name) == true &&
-                it.layer.currFrame == frameNumber
-        } ?: LayerSubject.assertThat(name, this)
+            it.layer?.run { predicate(this) } ?: false
+        } ?: LayerSubject.assertThat(name, this, timestamp)
     }
 
     override fun toString(): String {
@@ -237,26 +270,28 @@
          * Boiler-plate Subject.Factory for LayersTraceSubject
          */
         private fun getFactory(
-            trace: LayersTraceSubject? = null
+            trace: LayersTrace?,
+            parent: FlickerSubject?
         ): Factory<Subject, LayerTraceEntry> =
-            Factory { fm, subject -> LayerTraceEntrySubject(fm, subject, trace) }
+            Factory { fm, subject -> LayerTraceEntrySubject(fm, subject, trace, parent) }
 
         /**
          * Creates a [LayerTraceEntrySubject] to representing a SurfaceFlinger state[entry],
          * which can be used to make assertions.
          *
          * @param entry SurfaceFlinger trace entry
-         * @param trace Trace that contains this entry (optional)
+         * @param parent Trace that contains this entry (optional)
          */
         @JvmStatic
         @JvmOverloads
         fun assertThat(
             entry: LayerTraceEntry,
-            trace: LayersTraceSubject? = null
+            trace: LayersTrace? = null,
+            parent: FlickerSubject? = null
         ): LayerTraceEntrySubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(trace))
+                .about(getFactory(trace, parent))
                 .that(entry) as LayerTraceEntrySubject
             strategy.init(subject)
             return subject
@@ -267,8 +302,9 @@
          */
         @JvmStatic
         @JvmOverloads
-        fun entries(trace: LayersTraceSubject? = null): Factory<Subject, LayerTraceEntry> {
-            return getFactory(trace)
-        }
+        fun entries(
+            trace: LayersTrace? = null,
+            parent: FlickerSubject? = null
+        ): Factory<Subject, LayerTraceEntry> = getFactory(trace, parent)
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt
deleted file mode 100644
index d6a6810..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker.traces.layers
-
-import com.android.server.wm.traces.common.layers.LayerTraceEntry
-import com.android.server.wm.traces.parser.toAndroidRegion
-
-fun LayerTraceEntry.getVisibleBounds(layerName: String): android.graphics.Region {
-    return flattenedLayers.firstOrNull { it.name.contains(layerName) && it.isVisible }
-        ?.visibleRegion?.toAndroidRegion()
-        ?: android.graphics.Region()
-}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
index 9c7ba2a..39c8c62 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
@@ -22,9 +22,12 @@
 import com.android.server.wm.flicker.assertions.FlickerSubject
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.FlickerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.layers.Layer
 import com.android.server.wm.traces.common.layers.LayersTrace
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.server.wm.traces.parser.toAndroidRect
+import com.android.server.wm.traces.parser.toAndroidRegion
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.FailureStrategy
 import com.google.common.truth.StandardSubjectBuilder
@@ -56,20 +59,18 @@
  */
 class LayersTraceSubject private constructor(
     fm: FailureMetadata,
-    val trace: LayersTrace
+    val trace: LayersTrace,
+    override val parent: LayersTraceSubject?
 ) : FlickerTraceSubject<LayerTraceEntrySubject>(fm, trace) {
-    override val defaultFacts: String by lazy {
-        buildString {
-            if (trace.hasSource()) {
-                append("Path: ${trace.source}")
-                append("\n")
+    override val selfFacts
+        get() = super.selfFacts.toMutableList()
+            .also {
+                if (trace.hasSource()) {
+                    it.add(Fact.fact("Trace file", trace.source))
+                }
             }
-            append("Trace: $trace")
-        }
-    }
-
     override val subjects by lazy {
-        trace.entries.map { LayerTraceEntrySubject.assertThat(it, this) }
+        trace.entries.map { LayerTraceEntrySubject.assertThat(it, trace, this) }
     }
 
     /**
@@ -81,21 +82,11 @@
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return LayersTraceSubject(fm, trace)
+        return LayersTraceSubject(fm, trace, parent)
     }
 
-    /**
-     * Signal that the last assertion set is complete. The next assertion added will start a new
-     * set of assertions.
-     *
-     * E.g.: checkA().then().checkB()
-     *
-     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
-     * after checkA passes.
-     */
-    fun then(): LayersTraceSubject = apply {
-        startAssertionBlock()
-    }
+    /** {@inheritDoc} */
+    override fun then(): LayersTraceSubject = apply { super.then() }
 
     fun isEmpty(): LayersTraceSubject = apply {
         check("Trace is empty").that(trace).isEmpty()
@@ -113,206 +104,243 @@
         return subjects
             .map { it.layer(name, frameNumber) }
             .firstOrNull { it.isNotEmpty }
-            ?: LayerSubject.assertThat(null)
+            ?: LayerSubject.assertThat(null, this, timestamp = subjects.first().entry.timestamp)
+    }
+
+    /**
+     * @return List of [LayerSubject]s matching [name] in the order they appear on the trace
+     */
+    fun layers(name: String): List<LayerSubject> {
+        return subjects
+            .map { it.layer { layer -> layer.name.contains(name) } }
+            .filter { it.isNotEmpty }
+    }
+
+    /**
+     * @return List of [LayerSubject]s matching [predicate] in the order they appear on the trace
+     */
+    fun layers(predicate: (Layer) -> Boolean): List<LayerSubject> {
+        return subjects
+            .map { it.layer { layer -> predicate(layer) } }
+            .filter { it.isNotEmpty }
     }
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * [component] covers at least [testRegion], that is, if its area of the layer's visible
      * region covers each point in the region.
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtLeast(
         testRegion: Rect,
-        vararg layerName: String
-    ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName)
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtLeast(Region(testRegion), component)
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * [component] covers at least [testRegion], that is, if its area of the layer's visible
      * region covers each point in the region.
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtLeast(
         testRegion: com.android.server.wm.traces.common.Rect,
-        vararg layerName: String
-    ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName)
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtLeast(testRegion.toAndroidRect(), component)
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * [component] covers at most [testRegion], that is, if the area of any layer doesn't
      * cover any point outside of [testRegion].
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtMost(
         testRegion: Rect,
-        vararg layerName: String
-    ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName)
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtMost(Region(testRegion), component)
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * [component] covers at most [testRegion], that is, if the area of any layer doesn't
      * cover any point outside of [testRegion].
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtMost(
         testRegion: com.android.server.wm.traces.common.Rect,
-        vararg layerName: String
-    ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName)
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtMost(testRegion.toAndroidRect(), component)
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * [component] covers at least [testRegion], that is, if its area of the layer's visible
      * region covers each point in the region.
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtLeast(
         testRegion: Region,
-        vararg layerName: String
+        component: FlickerComponentName? = null
     ): LayersTraceSubject = apply {
-        addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") {
-            it.visibleRegion(*layerName).coversAtLeast(testRegion)
+        addAssertion("coversAtLeast($testRegion, ${component?.toLayerName()})") {
+            it.visibleRegion(component).coversAtLeast(testRegion)
         }
     }
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * [component] covers at least [testRegion], that is, if its area of the layer's visible
      * region covers each point in the region.
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtLeast(
         testRegion: com.android.server.wm.traces.common.Region,
-        vararg layerName: String
-    ): LayersTraceSubject = apply {
-        addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") {
-            it.visibleRegion(*layerName).coversAtLeast(testRegion)
-        }
-    }
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtLeast(testRegion.toAndroidRegion(), component)
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * [component] covers at most [testRegion], that is, if the area of any layer doesn't
      * cover any point outside of [testRegion].
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtMost(
         testRegion: Region,
-        vararg layerName: String
+        component: FlickerComponentName? = null
     ): LayersTraceSubject = apply {
-        addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") {
-            it.visibleRegion(*layerName).coversAtMost(testRegion)
+        addAssertion("coversAtMost($testRegion, ${component?.toLayerName()}") {
+            it.visibleRegion(component).coversAtMost(testRegion)
         }
     }
 
     /**
      * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
-     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * [component] covers at most [testRegion], that is, if the area of any layer doesn't
      * cover any point outside of [testRegion].
      *
      * @param testRegion Expected covered area
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
+    @JvmOverloads
     fun coversAtMost(
         testRegion: com.android.server.wm.traces.common.Region,
-        vararg layerName: String
-    ): LayersTraceSubject = apply {
-        addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") {
-            it.visibleRegion(*layerName).coversAtMost(testRegion)
-        }
-    }
+        component: FlickerComponentName? = null
+    ): LayersTraceSubject = this.coversAtMost(testRegion.toAndroidRegion(), component)
 
     /**
      * Checks that all visible layers are shown for more than one consecutive entry
      */
     @JvmOverloads
     fun visibleLayersShownMoreThanOneConsecutiveEntry(
-        ignoreLayers: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+        ignoreLayers: List<FlickerComponentName> = listOf(FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
     ): LayersTraceSubject = apply {
         visibleEntriesShownMoreThanOneConsecutiveTime { subject ->
             subject.entry.visibleLayers
-                .filter { ignoreLayers.none { layerName -> layerName in it.name } }
+                .filter { ignoreLayers.none { component -> component.toLayerName() in it.name } }
                 .map { it.name }
                 .toSet()
         }
     }
 
     /**
-     * Asserts that a [Layer] with [Layer.name] containing any of [layerName] has a visible region
+     * Asserts that a [Layer] with [Layer.name] containing any of [component] has a visible region
      * of exactly [expectedVisibleRegion] in trace entries.
      *
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      * @param expectedVisibleRegion Expected visible region of the layer
      */
+    @JvmOverloads
     fun coversExactly(
         expectedVisibleRegion: Region,
-        vararg layerName: String
+        component: FlickerComponentName? = null
     ): LayersTraceSubject = apply {
-        addAssertion("coversExactly(${layerName.joinToString(", ")}$expectedVisibleRegion)") {
-            it.visibleRegion(*layerName).coversExactly(expectedVisibleRegion)
+        addAssertion("coversExactly($component$expectedVisibleRegion)") {
+            it.visibleRegion(component).coversExactly(expectedVisibleRegion)
         }
     }
 
     /**
      * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name]
-     * containing [layerName].
+     * containing [component].
      *
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun notContains(vararg layerName: String): LayersTraceSubject =
+    @JvmOverloads
+    fun notContains(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): LayersTraceSubject =
         apply {
-            addAssertion("notContains(${layerName.joinToString(", ")})") {
-                it.notContains(*layerName)
+            addAssertion("notContains(${component.toLayerName()})", isOptional) {
+                it.notContains(component)
             }
         }
 
     /**
      * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of
-     * [layerName].
+     * [component].
      *
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun contains(vararg layerName: String): LayersTraceSubject =
-        apply { addAssertion("contains(${layerName.joinToString(", ")})") {
-            it.contains(*layerName) }
+    @JvmOverloads
+    fun contains(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): LayersTraceSubject =
+        apply { addAssertion("contains(${component.toLayerName()})", isOptional) {
+            it.contains(component) }
         }
 
     /**
      * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of
-     * [layerName] that is visible.
+     * [component] that is visible.
      *
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
-    fun isVisible(vararg layerName: String): LayersTraceSubject =
-        apply { addAssertion("isVisible(${layerName.joinToString(", ")})") {
-            it.isVisible(*layerName) }
+    @JvmOverloads
+    fun isVisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): LayersTraceSubject =
+        apply { addAssertion("isVisible(${component.toLayerName()})", isOptional) {
+            it.isVisible(component) }
         }
 
     /**
      * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name]
-     * containing [layerName] or that the layer is not visible .
+     * containing [component] or that the layer is not visible .
      *
-     * @param layerName Name of the layer to search
+     * @param component Name of the layer to search
      */
-    fun isInvisible(vararg layerName: String): LayersTraceSubject =
+    @JvmOverloads
+    fun isInvisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): LayersTraceSubject =
         apply {
-            addAssertion("hidesLayer(${layerName.joinToString(", ")})") {
-                it.isInvisible(*layerName)
+            addAssertion("isInvisible(${component.toLayerName()})", isOptional) {
+                it.isInvisible(component)
             }
         }
 
@@ -321,8 +349,9 @@
      */
     operator fun invoke(
         name: String,
+        isOptional: Boolean = false,
         assertion: Assertion<LayerTraceEntrySubject>
-    ): LayersTraceSubject = apply { addAssertion(name, assertion) }
+    ): LayersTraceSubject = apply { addAssertion(name, isOptional, assertion) }
 
     fun hasFrameSequence(name: String, frameNumbers: Iterable<Long>): LayersTraceSubject = apply {
         val firstFrame = frameNumbers.first()
@@ -370,8 +399,8 @@
         /**
          * Boiler-plate Subject.Factory for LayersTraceSubject
          */
-        private val FACTORY: Factory<Subject, LayersTrace> =
-            Factory { fm, subject -> LayersTraceSubject(fm, subject) }
+        private fun getFactory(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> =
+            Factory { fm, subject -> LayersTraceSubject(fm, subject, parent) }
 
         /**
          * Creates a [LayersTraceSubject] to representing a SurfaceFlinger trace,
@@ -380,10 +409,11 @@
          * @param trace SurfaceFlinger trace
          */
         @JvmStatic
-        fun assertThat(trace: LayersTrace): LayersTraceSubject {
+        @JvmOverloads
+        fun assertThat(trace: LayersTrace, parent: LayersTraceSubject? = null): LayersTraceSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(FACTORY)
+                .about(getFactory(parent))
                 .that(trace) as LayersTraceSubject
             strategy.init(subject)
             return subject
@@ -393,8 +423,8 @@
          * Static method for getting the subject factory (for use with assertAbout())
          */
         @JvmStatic
-        fun entries(): Factory<Subject, LayersTrace> {
-            return FACTORY
+        fun entries(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> {
+            return getFactory(parent)
         }
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt
index ea642ec..b7a55dd 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt
@@ -16,19 +16,18 @@
 
 package com.android.server.wm.flicker.traces.windowmanager
 
-import android.content.ComponentName
 import android.view.Display
+import androidx.annotation.VisibleForTesting
 import com.android.server.wm.flicker.assertions.Assertion
 import com.android.server.wm.flicker.assertions.FlickerSubject
-import com.android.server.wm.flicker.containsAny
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.Region
 import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 import com.android.server.wm.traces.common.windowmanager.windows.Activity
 import com.android.server.wm.traces.common.windowmanager.windows.WindowState
-import com.android.server.wm.traces.parser.toActivityName
 import com.android.server.wm.traces.parser.toAndroidRegion
-import com.android.server.wm.traces.parser.toWindowName
 import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.FailureStrategy
@@ -61,14 +60,31 @@
 class WindowManagerStateSubject private constructor(
     fm: FailureMetadata,
     val wmState: WindowManagerState,
-    val trace: WindowManagerTraceSubject?
+    val trace: WindowManagerTraceSubject?,
+    override val parent: FlickerSubject?
 ) : FlickerSubject(fm, wmState) {
-    override val defaultFacts = "${trace?.defaultFacts ?: ""}\nEntry: $wmState"
+    override val timestamp: Long get() = wmState.timestamp
+    override val selfFacts = listOf(Fact.fact("Entry", wmState))
 
     val subjects by lazy {
-        wmState.windowStates.map { WindowStateSubject.assertThat(it, this) }
+        wmState.windowStates.map { WindowStateSubject.assertThat(it, this, timestamp) }
     }
 
+    val appWindows: List<WindowStateSubject>
+        get() = subjects.filter { wmState.appWindows.contains(it.windowState) }
+
+    val nonAppWindows: List<WindowStateSubject>
+        get() = subjects.filter { wmState.nonAppWindows.contains(it.windowState) }
+
+    val aboveAppWindows: List<WindowStateSubject>
+        get() = subjects.filter { wmState.aboveAppWindows.contains(it.windowState) }
+
+    val belowAppWindows: List<WindowStateSubject>
+        get() = subjects.filter { wmState.belowAppWindows.contains(it.windowState) }
+
+    val visibleWindows: List<WindowStateSubject>
+        get() = subjects.filter { wmState.visibleWindows.contains(it.windowState) }
+
     /**
      * Executes a custom [assertion] on the current subject
      */
@@ -77,265 +93,188 @@
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return WindowManagerStateSubject(fm, wmState, trace)
+        return WindowManagerStateSubject(fm, wmState, trace, parent)
     }
 
     /**
-     * Asserts that the current WindowManager state doesn't contain [WindowState]s
+     * Asserts the current WindowManager state doesn't contain [WindowState]s
      */
     fun isEmpty(): WindowManagerStateSubject = apply {
-        check("State is empty")
-            .that(wmState.windowStates)
-            .isEmpty()
+        check("State is empty").that(subjects).isEmpty()
     }
 
     /**
-     * Asserts that the current WindowManager state contains [WindowState]s
+     * Asserts the current WindowManager state contains [WindowState]s
      */
     fun isNotEmpty(): WindowManagerStateSubject = apply {
-        check("State is not empty")
-            .that(wmState.windowStates)
-            .isNotEmpty()
+        check("State is not empty").that(subjects).isNotEmpty()
     }
 
     /**
-     * Obtains the region occupied by all windows with name containing any of [partialWindowTitles]
+     * Obtains the region occupied by all windows with name containing any of [component]
      *
-     * @param partialWindowTitles Name of the layer to search
+     * @param component Component to search
      */
-    fun frameRegion(vararg partialWindowTitles: String): RegionSubject {
-        val selectedWindows = subjects.filter { it.name.containsAny(*partialWindowTitles) }
+    fun frameRegion(component: FlickerComponentName?): RegionSubject {
+        val windowName = component?.toWindowName() ?: ""
+        val selectedWindows = subjects.filter { it.name.contains(windowName) }
 
         if (selectedWindows.isEmpty()) {
-            fail("Could not find", selectedWindows.joinToString(", "))
+            fail(Fact.fact(ASSERTION_TAG, "frameRegion(${component?.toWindowName() ?: "<any>"})"),
+                    Fact.fact("Could not find", windowName))
         }
 
         val visibleWindows = selectedWindows.filter { it.isVisible }
         val frameRegions = visibleWindows.mapNotNull { it.windowState?.frameRegion }.toTypedArray()
-        return RegionSubject.assertThat(frameRegions, selectedWindows)
+        return RegionSubject.assertThat(frameRegions, this)
     }
 
     /**
-     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
-     * containing any of [partialWindowTitles].
+     * Asserts the state contains a [WindowState] with title matching [component] above the
+     * app windows
      *
-     * @param partialWindowTitles window titles to search to search
+     * @param component Component to search
      */
-    fun contains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        val found = if (partialWindowTitles.isNotEmpty()) {
-            wmState.windowStates.any { it.name.containsAny(*partialWindowTitles) }
-        } else {
-            wmState.windowStates.isNotEmpty()
-        }
-
-        if (!found) {
-            fail("Could not find", partialWindowTitles.joinToString(", "))
-        }
+    fun containsAboveAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(aboveAppWindows, component)
     }
 
     /**
-     * Asserts that the WindowManager state doesn't contain a [WindowState] with
-     * [WindowState.title] containing [partialWindowTitles].
+     * Asserts the state contains a [WindowState] with title matching [component] below the
+     * app windows
      *
-     * @param partialWindowTitles Title of the window to search
+     * @param component Component to search
      */
-    fun notContains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        val found = wmState.windowStates.none { it.name.containsAny(*partialWindowTitles) }
-        if (!found) {
-            fail("Could find", partialWindowTitles.joinToString(", "))
-        }
+    fun containsBelowAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(belowAppWindows, component)
     }
 
     /**
-     * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] is visible.
-     *
-     * @param partialWindowTitles Title of the window to search
-     */
-    fun isVisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = true)
-    }
-
-    /**
-     * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] doesn't
-     * exist or is invisible.
-     *
-     * @param partialWindowTitles Title of the window to search
-     */
-    fun isInvisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = false)
-    }
-
-    private fun Array<WindowState>.checkIsVisible(vararg partialWindowTitles: String) {
-        this@WindowManagerStateSubject.contains(*partialWindowTitles)
-        val visibleWindows = this.filter { it.isVisible }
-            .filter { it.name.containsAny(*partialWindowTitles) }
-
-        if (visibleWindows.isEmpty()) {
-            fail("Is Invisible", partialWindowTitles.joinToString(", "))
-        }
-    }
-
-    private fun Array<WindowState>.checkIsInvisible(vararg partialWindowTitles: String) {
-        try {
-            notContains(*partialWindowTitles)
-        } catch (e: AssertionError) {
-            val invisibleWindows = this.filterNot { it.isVisible }
-                .filter { it.name.containsAny(*partialWindowTitles) }
-            if (invisibleWindows.isEmpty()) {
-                fail("Is Visible", partialWindowTitles.joinToString(", "))
-            }
-        }
-    }
-
-    private fun Array<WindowState>.checkVisibility(
-        vararg partialWindowTitles: String,
-        isVisible: Boolean
-    ) {
-        if (isVisible) {
-            checkIsVisible(*partialWindowTitles)
-        } else {
-            checkIsInvisible(*partialWindowTitles)
-        }
-    }
-
-    /**
-     * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title
-     * containing [partialWindowTitles] exists, is above all app windows ([WindowManagerState.appWindows])
-     * and has a visibility equal to [isVisible]
-     *
-     * This assertion can be used, for example, to assert that the Status and Navigation bars
-     * are visible and shown above the app
-     *
-     * @param partialWindowTitles window title to search
-     * @param isVisible if the found window should be visible or not
-     */
-    @JvmOverloads
-    fun isAboveAppWindow(
-        vararg partialWindowTitles: String,
-        isVisible: Boolean = true
-    ): WindowManagerStateSubject = apply {
-        wmState.aboveAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
-    }
-
-    /**
-     * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title
-     * containing [partialWindowTitles] exists, is below all app windows ([WindowManagerState.appWindows])
-     * and has a visibility equal to [isVisible]
-     *
-     * This assertion can be used, for example, to assert that the wallpaper is visible and
-     * shown below the app
-     *
-     * @param partialWindowTitles window title to search
-     * @param isVisible if the found window should be visible or not
-     */
-    @JvmOverloads
-    fun isBelowAppWindow(
-        vararg partialWindowTitles: String,
-        isVisible: Boolean = true
-    ): WindowManagerStateSubject = apply {
-        wmState.belowAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
-    }
-
-    /**
-     * Asserts that a window A with title containing [aboveWindowTitle] exists,
-     * a window B with title containing [belowWindowTitle] also exists, and that
-     * A is shown above B.
+     * Asserts the state contains [WindowState]s with titles matching [aboveWindowComponent] and
+     * [belowWindowComponent], and that [aboveWindowComponent] is above [belowWindowComponent]
      *
      * This assertion can be used, for example, to assert that a PIP window is shown above
      * other apps.
      *
-     * @param aboveWindowTitle name of the window that should be above
-     * @param belowWindowTitle name of the window that should be below
+     * @param aboveWindowComponent name of the window that should be above
+     * @param belowWindowComponent name of the window that should be below
      */
-    fun isAboveWindow(aboveWindowTitle: String, belowWindowTitle: String) {
+    fun isAboveWindow(
+        aboveWindowComponent: FlickerComponentName,
+        belowWindowComponent: FlickerComponentName
+    ): WindowManagerStateSubject = apply {
+        contains(aboveWindowComponent)
+        contains(belowWindowComponent)
+
         // windows are ordered by z-order, from top to bottom
+        val aboveWindowTitle = aboveWindowComponent.toWindowName()
+        val belowWindowTitle = belowWindowComponent.toWindowName()
         val aboveZ = wmState.windowStates.indexOfFirst { aboveWindowTitle in it.name }
         val belowZ = wmState.windowStates.indexOfFirst { belowWindowTitle in it.name }
-
-        contains(aboveWindowTitle)
-        contains(belowWindowTitle)
         if (aboveZ >= belowZ) {
-            fail("$aboveWindowTitle is above $belowWindowTitle")
+            val aboveWindow = subjects.first { aboveWindowTitle in it.name }
+            aboveWindow.fail(Fact.fact(ASSERTION_TAG, "isAboveWindow(above=$aboveWindowTitle, " +
+                "below=$belowWindowTitle"),
+                Fact.fact("Above", aboveWindowTitle),
+                Fact.fact("Below", belowWindowTitle))
         }
     }
 
     /**
-     * Asserts that the WindowManager state contains a non-app [WindowState] with
-     * [WindowState.title] containing [partialWindowTitles] and that its visibility is
-     * equal to [isVisible]
+     * Asserts the state contains a non-app [WindowState] with title matching [component]
      *
-     * @param partialWindowTitles window title to search
-     * @param isVisible if the found window should be visible or not
+     * @param component Component to search
      */
-    @JvmOverloads
-    fun containsNonAppWindow(
-        vararg partialWindowTitles: String,
-        isVisible: Boolean = true
+    fun containsNonAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(nonAppWindows, component)
+    }
+
+    /**
+     * Asserts the title of the top visible app window in the state contains [component]
+     *
+     * @param component Component to search
+     */
+    fun isAppWindowOnTop(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        val windowName = component.toWindowName()
+        if (!wmState.topVisibleAppWindow.contains(windowName)) {
+            val topWindow = subjects.first { it.name == wmState.topVisibleAppWindow }
+            topWindow.fail(
+                Fact.fact(ASSERTION_TAG, "isAppWindowOnTop(${component.toWindowName()})"),
+                Fact.fact("Not on top", component.toWindowName()),
+                Fact.fact("Found", wmState.topVisibleAppWindow)
+            )
+        }
+    }
+
+    /**
+     * Asserts the title of the top visible app window in the state contains [component]
+     *
+     * @param component Component to search
+     */
+    fun isAppWindowNotOnTop(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        val windowName = component.toWindowName()
+        if (wmState.topVisibleAppWindow.contains(windowName)) {
+            val topWindow = subjects.first { it.name == wmState.topVisibleAppWindow }
+            topWindow.fail(
+                Fact.fact(ASSERTION_TAG, "isAppWindowNotOnTop(${component.toWindowName()})"),
+                Fact.fact("On top", component.toWindowName())
+            )
+        }
+    }
+
+    /**
+     * Asserts the bounds of the [WindowState]s title matching [component] don't overlap.
+     *
+     * @param component Component to search
+     */
+    fun doNotOverlap(
+        vararg component: FlickerComponentName
     ): WindowManagerStateSubject = apply {
-        wmState.nonAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
-    }
-
-    /**
-     * Asserts that the title of the top visible app window in the state contains any
-     * of [partialWindowTitles]
-     *
-     * @param partialWindowTitles window title to search
-     */
-    fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        contains(*partialWindowTitles)
-        val windowOnTop = wmState.topVisibleAppWindow.containsAny(*partialWindowTitles)
-
-        if (!windowOnTop) {
-            fail(Fact.fact("Not on top", partialWindowTitles.joinToString(", ")),
-                Fact.fact("Found", wmState.topVisibleAppWindow))
-        }
-    }
-
-    /**
-     * Asserts that the [WindowState.bounds] of the [WindowState] with [WindowState.title]
-     * contained in any of [partialWindowTitles] don't overlap.
-     *
-     * @param partialWindowTitles Title of the windows that should not overlap
-     */
-    fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
-        partialWindowTitles.forEach { contains(it) }
-        val foundWindows = partialWindowTitles.toSet()
-            .associateWith { title -> wmState.windowStates.find { it.name.contains(title) } }
+        component.forEach { contains(it) }
+        val foundWindows = component.toSet()
+            .associateWith { act ->
+                wmState.windowStates.firstOrNull { it.name.contains(act.toWindowName()) }
+            }
             // keep entries only for windows that we actually found by removing nulls
             .filterValues { it != null }
-            .mapValues { (_, v) -> v!!.frameRegion }
+        val foundWindowsRegions = foundWindows
+            .mapValues { (_, v) -> v?.frameRegion ?: Region.EMPTY }
 
-        val regions = foundWindows.entries.toList()
+        val regions = foundWindowsRegions.entries.toList()
         for (i in regions.indices) {
             val (ourTitle, ourRegion) = regions[i]
             for (j in i + 1 until regions.size) {
                 val (otherTitle, otherRegion) = regions[j]
                 if (ourRegion.toAndroidRegion().op(otherRegion.toAndroidRegion(),
                         android.graphics.Region.Op.INTERSECT)) {
-                    fail(Fact.fact("Overlap", ourTitle), Fact.fact("Overlap", otherTitle))
+                    val window = foundWindows[ourTitle] ?: error("Window $ourTitle not found")
+                    val windowSubject = subjects.first { it.windowState == window }
+                    windowSubject.fail(Fact.fact(ASSERTION_TAG,
+                            "noWindowsOverlap${component.joinToString { it.toWindowName() }}"),
+                        Fact.fact("Overlap", ourTitle),
+                        Fact.fact("Overlap", otherTitle))
                 }
             }
         }
     }
 
     /**
-     * Asserts that the WindowManager state contains an app [WindowState] with
-     * [WindowState.title] containing [partialWindowTitles] and that its visibility
-     * is equal to [isVisible]
+     * Asserts the state contains an app [WindowState] with title matching [component]
      *
-     * @param partialWindowTitles window title to search
-     * @param isVisible if the found window should be visible or not
+     * @param component Component to search
      */
-    @JvmOverloads
-    fun containsAppWindow(
-        vararg partialWindowTitles: String,
-        isVisible: Boolean = true
-    ): WindowManagerStateSubject = apply {
-        wmState.appWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
+    fun containsAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        val windowName = component.toWindowName()
+        // Check existence of activity
+        val activity = wmState.getActivitiesForWindow(windowName).firstOrNull()
+        check("Activity for window $windowName must exist.")
+            .that(activity).isNotNull()
+        // Check existence of window.
+        contains(component)
     }
 
     /**
-     * Asserts that the display with id [displayId] has rotation [rotation]
+     * Asserts the display with id [displayId] has rotation [rotation]
      *
      * @param rotation to assert
      * @param displayId of the target display
@@ -351,72 +290,69 @@
     }
 
     /**
-     * Asserts that the display with id [displayId] has rotation [rotation]
+     * Asserts the state contains a [WindowState] with title matching [component].
      *
-     * @param rotation to assert
-     * @param displayId of the target display
+     * @param component Component name to search
      */
-    @JvmOverloads
-    fun isNotRotation(
-        rotation: Int,
-        displayId: Int = Display.DEFAULT_DISPLAY
-    ): WindowManagerStateSubject = apply {
-        check("Rotation should not be $rotation")
-            .that(rotation)
-            .isNotEqualTo(wmState.getRotation(displayId))
+    fun contains(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(subjects, component)
     }
 
     /**
-     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
-     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
-     * [ComponentName.toActivityName]
+     * Asserts the state doesn't contain a [WindowState] nor an [Activity] with title
+     * matching [component].
      *
-     * @param activity Component name to search
+     * @param component Component name to search
      */
-    fun contains(activity: ComponentName): WindowManagerStateSubject = apply {
-        val windowName = activity.toWindowName()
-        val activityName = activity.toActivityName()
-        check("Activity=$activityName must exist.")
-            .that(wmState.containsActivity(activityName)).isTrue()
-        check("Window=$windowName must exits.")
-            .that(wmState.containsWindow(windowName)).isTrue()
-    }
-
-    /**
-     * Asserts that the WindowManager state doesn't contain a [WindowState] with [WindowState.title]
-     * equal to [ComponentName.toWindowName] nor an [Activity] with [Activity.title] equal to
-     * [ComponentName.toActivityName]
-     *
-     * @param activity Component name to search
-     */
-    fun notContains(activity: ComponentName): WindowManagerStateSubject = apply {
-        val windowName = activity.toWindowName()
-        val activityName = activity.toActivityName()
+    fun notContainsAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        val activityName = component.toActivityName()
+        // system components (e.g., NavBar, StatusBar, PipOverlay) don't have a package name
+        // nor an activity, ignore them
         check("Activity=$activityName must NOT exist.")
             .that(wmState.containsActivity(activityName)).isFalse()
+        notContains(component)
+    }
+
+    /**
+     * Asserts the state doesn't contain a [WindowState] with title matching [component].
+     *
+     * @param component Component name to search
+     */
+    fun notContains(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        val windowName = component.toWindowName()
         check("Window=$windowName must NOT exits.")
             .that(wmState.containsWindow(windowName)).isFalse()
     }
 
-    @JvmOverloads
-    fun isRecentsActivityVisible(visible: Boolean = true): WindowManagerStateSubject = apply {
+    fun isRecentsActivityVisible(): WindowManagerStateSubject = apply {
         if (wmState.isHomeRecentsComponent) {
             isHomeActivityVisible()
         } else {
-            check("Recents activity is ${if (visible) "" else "not"} visible")
+            check("Recents activity visibility")
                 .that(wmState.isRecentsActivityVisible)
-                .isEqualTo(visible)
+                .isTrue()
+        }
+    }
+
+    fun isRecentsActivityInvisible(): WindowManagerStateSubject = apply {
+        if (wmState.isHomeRecentsComponent) {
+            isHomeActivityInvisible()
+        } else {
+            check("Recents activity visibility")
+                .that(wmState.isRecentsActivityVisible)
+                .isFalse()
         }
     }
 
     /**
-     * Asserts that the WindowManager state is valid, that is, if it has:
+     * Asserts the state is valid, that is, if it has:
      *   - a resumed activity
      *   - a focused activity
      *   - a focused window
      *   - a front window
      *   - a focused app
      */
+    @VisibleForTesting
     fun isValid(): WindowManagerStateSubject = apply {
         check("Must have stacks").that(wmState.stackCount).isGreaterThan(0)
         // TODO: Update when keyguard will be shown on multiple displays
@@ -442,229 +378,171 @@
     }
 
     /**
-     * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp]
-     * match [activity]
+     * Asserts the state contains a visible window with [WindowState.title] matching [component].
      *
-     * @param activity Component name to search
+     * Also, if [component] has a package name (i.e., is not a system component), also checks that
+     * it contains a visible [Activity] with [Activity.title] matching [component].
+     *
+     * @param component Component name to search
      */
-    fun hasFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
-        val activityComponentName = activity.toActivityName()
-        check("Focused activity invalid")
-            .that(activityComponentName)
-            .isEqualTo(wmState.focusedActivity)
-        check("Focused app invalid")
-            .that(activityComponentName)
-            .isEqualTo(wmState.focusedApp)
+    fun isNonAppWindowVisible(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        checkWindowVisibility("isVisible", nonAppWindows, component, isVisible = true)
     }
 
     /**
-     * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp]
-     * don't match [activity]
+     * Asserts the state contains a visible window with [WindowState.title] matching [component].
      *
-     * @param activity Component name to search
-     */
-    fun hasNotFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
-        val activityComponentName = activity.toActivityName()
-        check("Has focused activity")
-            .that(wmState.focusedActivity)
-            .isNotEqualTo(activityComponentName)
-        check("Has focused app")
-            .that(wmState.focusedApp)
-            .isNotEqualTo(activityComponentName)
-    }
-
-    /**
-     * Asserts that the display [displayId] has a [WindowManagerState.focusedApp]
-     * matching [activity]
+     * Also, if [component] has a package name (i.e., is not a system component), also checks that
+     * it contains a visible [Activity] with [Activity.title] matching [component].
      *
-     * @param activity Component name to search
+     * @param component Component name to search
      */
-    @JvmOverloads
-    fun hasFocusedApp(
-        activity: ComponentName,
-        displayId: Int = Display.DEFAULT_DISPLAY
+    fun isAppWindowVisible(
+        component: FlickerComponentName
     ): WindowManagerStateSubject = apply {
-        val activityComponentName = activity.toActivityName()
-        check("Focused app invalid")
-            .that(activityComponentName)
-            .isEqualTo(wmState.getDisplay(displayId)?.focusedApp)
+        containsAppWindow(component)
+
+        val windowName = component.toWindowName()
+        // Check existence of activity
+        val activity = wmState.getActivitiesForWindow(windowName).firstOrNull()
+        // Check visibility of activity and window.
+        check("Activity=${activity?.name} must be visible.")
+            .that(activity?.isVisible ?: false).isTrue()
+        checkWindowVisibility("isVisible", appWindows, component, isVisible = true)
     }
 
     /**
-     * Asserts that WindowManager state has a [WindowManagerState.resumedActivities]
-     * matching [activity]
+     * Asserts the state contains an invisible window with [WindowState.title] matching [component].
      *
-     * @param activity Component name to search
-     */
-    fun hasResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
-        val activityComponentName = activity.toActivityName()
-        check("Invalid resumed activity")
-            .that(wmState.resumedActivities)
-            .asList()
-            .contains(activityComponentName)
-    }
-
-    /**
-     * Asserts that WindowManager state [WindowManagerState.resumedActivities] doesn't
-     * match [activity]
+     * Also, if [component] has a package name (i.e., is not a system component), also checks that
+     * it contains an invisible [Activity] with [Activity.title] matching [component].
      *
-     * @param activity Component name to search
+     * @param component Component name to search
      */
-    fun hasNotResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
-        val activityComponentName = activity.toActivityName()
-        check("Has resumed activity")
-            .that(wmState.resumedActivities)
-            .asList()
-            .doesNotContain(activityComponentName)
-    }
-
-    /**
-     * Asserts that title of the [WindowManagerState.focusedWindow] on the state matches
-     * [windowTitle]
-     *
-     * @param windowTitle window title to search
-     */
-    fun isFocused(windowTitle: String): WindowManagerStateSubject = apply {
-        check("Invalid focused window")
-            .that(windowTitle)
-            .isEqualTo(wmState.focusedWindow)
-    }
-
-    /**
-     * Asserts that [WindowManagerState.focusedWindow] on the WindowManager state doesn't
-     * match [windowTitle]
-     *
-     * @param windowTitle window title to search
-     */
-    fun isWindowNotFocused(windowTitle: String): WindowManagerStateSubject = apply {
-        check("Has focused window")
-            .that(wmState.focusedWindow)
-            .isNotEqualTo(windowTitle)
-    }
-
-    /**
-     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
-     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
-     * [ComponentName.toActivityName] and both are visible
-     *
-     * @param activity Component name to search
-     */
-    fun isVisible(activity: ComponentName): WindowManagerStateSubject =
-        hasActivityAndWindowVisibility(activity, visible = true)
-
-    /**
-     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
-     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
-     * [ComponentName.toActivityName] and both are invisible
-     *
-     * @param activity Component name to search
-     */
-    fun isInvisible(activity: ComponentName): WindowManagerStateSubject =
-        hasActivityAndWindowVisibility(activity, visible = false)
-
-    private fun hasActivityAndWindowVisibility(
-        activity: ComponentName,
-        visible: Boolean
+    fun isAppWindowInvisible(
+        component: FlickerComponentName
     ): WindowManagerStateSubject = apply {
-        // Check existence of activity and window.
-        val windowName = activity.toWindowName()
-        val activityName = activity.toActivityName()
-        check("Activity=$activityName must exist.")
-            .that(wmState.containsActivity(activityName)).isTrue()
+        val activityName = component.toActivityName()
+
+        // system components (e.g., NavBar, StatusBar, PipOverlay) don't have a package name
+        // nor an activity, ignore them
+        // activity is visible, check window
+        if (wmState.isActivityVisible(activityName)) {
+            checkWindowVisibility("isInvisible", appWindows, component, isVisible = false)
+        }
+    }
+
+    /**
+     * Asserts the state contains an invisible window with [WindowState.title] matching [component].
+     *
+     * Also, if [component] has a package name (i.e., is not a system component), also checks that
+     * it contains an invisible [Activity] with [Activity.title] matching [component].
+     *
+     * @param component Component name to search
+     */
+    fun isNonAppWindowInvisible(
+        component: FlickerComponentName
+    ): WindowManagerStateSubject = apply {
+        checkWindowVisibility("isInvisible", nonAppWindows, component, isVisible = false)
+    }
+
+    private fun checkWindowVisibility(
+        assertionName: String,
+        subjectList: List<WindowStateSubject>,
+        component: FlickerComponentName,
+        isVisible: Boolean
+    ) {
+        // Check existence of window.
+        contains(subjectList, component)
+
+        val windowName = component.toWindowName()
+        val foundWindows = subjectList.filter { it.name.contains(windowName) }
+        val windowsWithVisibility = foundWindows.filter { it.isVisible == isVisible }
+
+        if (windowsWithVisibility.isEmpty()) {
+            val errorTag = if (isVisible) "Is Invisible" else "Is Visible"
+            val facts = listOf<Fact>(
+                Fact.fact(ASSERTION_TAG, "$assertionName(${component.toWindowName()})"),
+                Fact.fact(errorTag, windowName)
+            )
+            foundWindows.first().fail(facts)
+        }
+    }
+
+    private fun contains(subjectList: List<WindowStateSubject>, component: FlickerComponentName) {
+        val windowName = component.toWindowName()
         check("Window=$windowName must exist.")
             .that(wmState.containsWindow(windowName)).isTrue()
-
-        // Check visibility of activity and window.
-        check("Activity=$activityName must ${if (visible) "" else " NOT"} be visible.")
-            .that(visible).isEqualTo(wmState.isActivityVisible(activityName))
-        check("Window=$windowName must ${if (visible) "" else " NOT"} have shown surface.")
-            .that(visible).isEqualTo(wmState.isWindowSurfaceShown(windowName))
     }
 
     /**
-     * Asserts that the WindowManager state home activity visibility is equal to [isVisible]
+     * Asserts the state home activity is visible
+     */
+    fun isHomeActivityVisible(): WindowManagerStateSubject = apply {
+        val homeIsVisible = wmState.homeActivity?.isVisible ?: false
+        check("Home activity doesn't exist").that(wmState.homeActivity).isNotNull()
+        check("Home activity is not visible").that(homeIsVisible).isTrue()
+    }
+
+    /**
+     * Asserts the state home activity is invisible
+     */
+    fun isHomeActivityInvisible(): WindowManagerStateSubject = apply {
+        val homeIsVisible = wmState.homeActivity?.isVisible ?: false
+        check("Home activity is visible").that(homeIsVisible).isFalse()
+    }
+
+    /**
+     * Asserts that [component] exists and is pinned (in PIP mode)
      *
-     * @param isVisible if the home activity should be visible of not
+     * @param component Component name to search
      */
-    @JvmOverloads
-    fun isHomeActivityVisible(isVisible: Boolean = true): WindowManagerStateSubject = apply {
-        if (isVisible) {
-            check("Home activity doesn't exist")
-                .that(wmState.homeActivity)
-                .isNotNull()
-
-            check("Home activity is not visible")
-                .that(wmState.homeActivity?.isVisible)
-                .isTrue()
-        } else {
-            check("Home activity is visible")
-                .that(wmState.homeActivity?.isVisible ?: false)
-                .isFalse()
-        }
+    fun isPinned(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(component)
+        val windowName = component.toWindowName()
+        val pinnedWindows = wmState.pinnedWindows.map { it.title }
+        check("Window not in PIP mode").that(pinnedWindows).contains(windowName)
     }
 
     /**
-     * Asserts that the IME surface is visible in the display [displayId]
+     * Asserts that [component] exists and is not pinned (not in PIP mode)
+     *
+     * @param component Component name to search
      */
-    @JvmOverloads
-    fun isImeWindowVisible(
-        displayId: Int = Display.DEFAULT_DISPLAY
-    ): WindowManagerStateSubject = apply {
-        val imeWinState = wmState.inputMethodWindowState
-        check("IME window must exist")
-            .that(imeWinState).isNotNull()
-        check("IME window must be shown")
-            .that(imeWinState?.isSurfaceShown ?: false).isTrue()
-        check("IME window must be on the given display")
-            .that(displayId).isEqualTo(imeWinState?.displayId ?: -1)
+    fun isNotPinned(component: FlickerComponentName): WindowManagerStateSubject = apply {
+        contains(component)
+        val windowName = component.toWindowName()
+        val pinnedWindows = wmState.pinnedWindows.map { it.title }
+        check("Window not in PIP mode").that(pinnedWindows).doesNotContain(windowName)
     }
 
     /**
-     * Asserts that the IME surface is invisible in the display [displayId]
+     * Obtains the first subject with [WindowState.title] containing [name].
+     *
+     * Always returns a subject, event when the layer doesn't exist. To verify if layer
+     * actually exists in the hierarchy use [WindowStateSubject.exists] or
+     * [WindowStateSubject.doesNotExist]
      */
-    @JvmOverloads
-    fun isImeWindowInvisible(
-        displayId: Int = Display.DEFAULT_DISPLAY
-    ): WindowManagerStateSubject = apply {
-        val imeWinState = wmState.inputMethodWindowState
-        check("IME window must not be shown")
-            .that(imeWinState?.isSurfaceShown ?: false).isFalse()
-        if (imeWinState?.isSurfaceShown == true) {
-            check("IME window must not be on the given display")
-                .that(displayId).isNotEqualTo(imeWinState.displayId)
-        }
+    fun windowState(name: String): WindowStateSubject {
+        return subjects.firstOrNull {
+            it.windowState?.name?.contains(name) == true
+        } ?: WindowStateSubject.assertThat(name, this, timestamp)
     }
 
     /**
-     * Asserts that an activity [activity] exists and is in PIP mode
-     */
-    fun isInPipMode(
-        activity: ComponentName
-    ): WindowManagerStateSubject = apply {
-        val windowName = activity.toWindowName()
-        contains(windowName)
-        val pinnedWindows = wmState.pinnedWindows
-            .map { it.title }
-        check("Window not in PIP mode")
-            .that(pinnedWindows)
-            .contains(windowName)
-    }
-
-    /**
-     * Obtains a [WindowStateSubject] for the first occurrence of a [WindowState] with
-     * [WindowState.title] containing [name].
+     * Obtains the first subject matching  [predicate].
      *
      * Always returns a subject, event when the layer doesn't exist. To verify if layer
      * actually exists in the hierarchy use [WindowStateSubject.exists] or
      * [WindowStateSubject.doesNotExist]
      *
-     * @return WindowStateSubject that can be used to make assertions on a single [WindowState]
-     * matching [name].
+     * @param predicate to search for a subject
+     * @param name Name of the subject to use when not found (optional)
      */
-    fun windowState(name: String): WindowStateSubject {
+    fun windowState(name: String = "", predicate: (WindowState) -> Boolean): WindowStateSubject {
         return subjects.firstOrNull {
-            it.windowState?.name?.contains(name) == true
-        } ?: WindowStateSubject.assertThat(name, this)
+            it.windowState?.run { predicate(this) } ?: false
+        } ?: WindowStateSubject.assertThat(name, this, timestamp)
     }
 
     override fun toString(): String {
@@ -675,28 +553,30 @@
         /**
          * Boiler-plate Subject.Factory for WindowManagerStateSubject
          *
-         * @param trace containing the entry
+         * @param parent containing the entry
          */
         private fun getFactory(
-            trace: WindowManagerTraceSubject? = null
+            trace: WindowManagerTraceSubject?,
+            parent: FlickerSubject?
         ): Factory<Subject, WindowManagerState> =
-            Factory { fm, subject -> WindowManagerStateSubject(fm, subject, trace) }
+            Factory { fm, subject -> WindowManagerStateSubject(fm, subject, trace, parent) }
 
         /**
          * User-defined entry point
          *
          * @param entry to assert
-         * @param trace containing the entry
+         * @param parent containing the entry
          */
         @JvmStatic
         @JvmOverloads
         fun assertThat(
             entry: WindowManagerState,
-            trace: WindowManagerTraceSubject? = null
+            trace: WindowManagerTraceSubject? = null,
+            parent: FlickerSubject? = null
         ): WindowManagerStateSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(trace))
+                .about(getFactory(trace, parent))
                 .that(entry) as WindowManagerStateSubject
             strategy.init(subject)
             return subject
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
index c790f97..ed77b20 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
@@ -20,11 +20,12 @@
 import com.android.server.wm.flicker.assertions.FlickerSubject
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.FlickerTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.Region
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.traces.common.windowmanager.windows.WindowState
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.FailureStrategy
 import com.google.common.truth.StandardSubjectBuilder
@@ -55,46 +56,32 @@
  */
 class WindowManagerTraceSubject private constructor(
     fm: FailureMetadata,
-    val trace: WindowManagerTrace
+    val trace: WindowManagerTrace,
+    override val parent: WindowManagerTraceSubject?
 ) : FlickerTraceSubject<WindowManagerStateSubject>(fm, trace) {
-    override val defaultFacts: String = buildString {
-        if (trace.hasSource()) {
-            append("Path: ${trace.source}")
-            append("\n")
-        }
-        append("Trace: $trace")
-    }
+    override val selfFacts
+        get() = super.selfFacts.toMutableList()
+            .also {
+                if (trace.hasSource()) {
+                    it.add(Fact.fact("Trace file", trace.source))
+                }
+            }
 
     override val subjects by lazy {
-        trace.entries.map { WindowManagerStateSubject.assertThat(it, this) }
+        trace.entries.map { WindowManagerStateSubject.assertThat(it, this, this) }
     }
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return WindowManagerTraceSubject(fm, trace)
+        return WindowManagerTraceSubject(fm, trace, parent)
     }
 
-    /**
-     * Signal that the last assertion set is complete. The next assertion added will start a new
-     * set of assertions.
-     *
-     * E.g.: checkA().then().checkB()
-     *
-     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
-     * after checkA passes.
-     */
-    fun then(): WindowManagerTraceSubject =
-        apply { startAssertionBlock() }
+    /** {@inheritDoc} */
+    override fun then(): WindowManagerTraceSubject = apply { super.then() }
 
-    /**
-     * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
-     * end of the trace without passing any assertion, return a failure with the name/reason from
-     * the first assertion
-     *
-     * @return
-     */
-    fun skipUntilFirstAssertion(): WindowManagerTraceSubject =
-        apply { assertionsChecker.skipUntilFirstAssertion() }
+    /** {@inheritDoc} */
+    override fun skipUntilFirstAssertion(): WindowManagerTraceSubject =
+        apply { super.skipUntilFirstAssertion() }
 
     fun isEmpty(): WindowManagerTraceSubject = apply {
         check("Trace is empty").that(trace).isEmpty()
@@ -105,353 +92,439 @@
     }
 
     /**
-     * Checks if the non-app window with title containing [partialWindowTitle] exists above the app
+     * @return List of [WindowStateSubject]s matching [partialWindowTitle] in the order they
+     *      appear on the trace
+     */
+    fun windowStates(partialWindowTitle: String): List<WindowStateSubject> {
+        return subjects
+            .map { it.windowState { windows -> windows.title.contains(partialWindowTitle) } }
+            .filter { it.isNotEmpty }
+    }
+
+    /**
+     * @return List of [WindowStateSubject]s matching [predicate] in the order they
+     *      appear on the trace
+     */
+    fun windowStates(predicate: (WindowState) -> Boolean): List<WindowStateSubject> {
+        return subjects
+            .map { it.windowState { window -> predicate(window) } }
+            .filter { it.isNotEmpty }
+    }
+
+    /** {@inheritDoc} */
+    fun notContains(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("notContains(${component.toWindowName()})", isOptional) {
+            it.notContains(component)
+        }
+    }
+
+    /**
+     * Checks if the non-app window with title containing [component] exists above the app
      * windows and is visible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun showsAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("showsAboveAppWindow($partialWindowTitle)") {
-            it.isAboveAppWindow(*partialWindowTitle)
+    @JvmOverloads
+    fun isAboveAppWindowVisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isAboveAppWindowVisible(${component.toWindowName()})", isOptional) {
+            it.containsAboveAppWindow(component)
+                .isNonAppWindowVisible(component)
         }
     }
 
     /**
-     * Checks if the non-app window with title containing [partialWindowTitle] exists above the app
+     * Checks if the non-app window with title containing [component] exists above the app
      * windows and is invisible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun hidesAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("hidesAboveAppWindow($partialWindowTitle)") {
-            it.isAboveAppWindow(*partialWindowTitle, isVisible = false)
+    @JvmOverloads
+    fun isAboveAppWindowInvisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isAboveAppWindowInvisible(${component.toWindowName()})", isOptional) {
+            it.containsAboveAppWindow(component)
+                .isNonAppWindowInvisible(component)
         }
     }
 
     /**
-     * Checks if the non-app window with title containing [partialWindowTitle] exists below the app
+     * Checks if the non-app window with title containing [component] exists below the app
      * windows and is visible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun showsBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("showsBelowAppWindow($partialWindowTitle)") {
-            it.isBelowAppWindow(*partialWindowTitle)
+    @JvmOverloads
+    fun isBelowAppWindowVisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isBelowAppWindowVisible(${component.toWindowName()})", isOptional) {
+            it.containsBelowAppWindow(component)
+                .isNonAppWindowVisible(component)
         }
     }
 
     /**
-     * Checks if the non-app window with title containing [partialWindowTitle] exists below the app
+     * Checks if the non-app window with title containing [component] exists below the app
      * windows and is invisible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun hidesBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("hidesBelowAppWindow($partialWindowTitle)") {
-            it.isBelowAppWindow(*partialWindowTitle, isVisible = false)
+    @JvmOverloads
+    fun isBelowAppWindowInvisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isBelowAppWindowInvisible(${component.toWindowName()})", isOptional) {
+            it.containsBelowAppWindow(component)
+                .isNonAppWindowInvisible(component)
         }
     }
 
     /**
-     * Checks if non-app window with title containing the [partialWindowTitle] exists above or
+     * Checks if non-app window with title containing the [component] exists above or
      * below the app windows and is visible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun showsNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("showsNonAppWindow($partialWindowTitle)") {
-            it.containsNonAppWindow(*partialWindowTitle)
+    @JvmOverloads
+    fun isNonAppWindowVisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isNonAppWindowVisible(${component.toWindowName()})", isOptional) {
+            it.isNonAppWindowVisible(component)
         }
     }
 
     /**
-     * Checks if non-app window with title containing the [partialWindowTitle] exists above or
+     * Checks if non-app window with title containing the [component] exists above or
      * below the app windows and is invisible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun hidesNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("hidesNonAppWindow($partialWindowTitle)") {
-            it.containsNonAppWindow(*partialWindowTitle, isVisible = false)
+    @JvmOverloads
+    fun isNonAppWindowInvisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isNonAppWindowInvisible(${component.toWindowName()})", isOptional) {
+            it.isNonAppWindowInvisible(component)
         }
     }
 
     /**
-     * Checks if an app window with title containing the [partialWindowTitles] is on top
+     * Checks if app window with title containing the [component] is on top
      *
-     * @param partialWindowTitles window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply {
-        val assertionName = "showsAppWindowOnTop(${partialWindowTitles.joinToString(",")})"
-        addAssertion(assertionName) {
-            check("No window titles to search")
-                .that(partialWindowTitles)
-                .isNotEmpty()
-            it.showsAppWindowOnTop(*partialWindowTitles)
+    @JvmOverloads
+    fun isAppWindowOnTop(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isAppWindowOnTop(${component.toWindowName()})", isOptional) {
+            it.isAppWindowOnTop(component)
         }
     }
 
     /**
-     * Checks if app window with title containing the [partialWindowTitle] is not on top
+     * Checks if app window with title containing the [component] is not on top
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun appWindowNotOnTop(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("hidesAppWindowOnTop($partialWindowTitle)") {
-            it.containsAppWindow(*partialWindowTitle, isVisible = false)
+    @JvmOverloads
+    fun isAppWindowNotOnTop(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("appWindowNotOnTop(${component.toWindowName()})", isOptional) {
+            it.isAppWindowNotOnTop(component)
         }
     }
 
     /**
-     * Checks if app window with title containing the [partialWindowTitle] is visible
+     * Checks if app window with title containing the [component] is visible
      *
-     * @param partialWindowTitle window title to search
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun showsAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("showsAppWindow($partialWindowTitle)") {
-            it.containsAppWindow(*partialWindowTitle, isVisible = true)
+    @JvmOverloads
+    fun isAppWindowVisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isAppWindowVisible(${component.toWindowName()})", isOptional) {
+            it.isAppWindowVisible(component)
         }
     }
 
     /**
-     * Checks if app window with title containing the [partialWindowTitle] is invisible
+     * Checks if app window with title containing the [component] is invisible
      *
-     * @param partialWindowTitle window title to search
+     * Note: This assertion have issues with the launcher window, because it contains 2 windows
+     * with the same name and only 1 is visible at a time. Prefer [isAppWindowOnTop] for launcher
+     * instead
+     *
+     * @param component Component to search
+     * @param isOptional If this assertion is optional or must pass
      */
-    fun hidesAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
-        addAssertion("hidesAppWindow($partialWindowTitle)") {
-            it.containsAppWindow(*partialWindowTitle, isVisible = false)
+    @JvmOverloads
+    fun isAppWindowInvisible(
+        component: FlickerComponentName,
+        isOptional: Boolean = false
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("isAppWindowInvisible(${component.toWindowName()})", isOptional) {
+            it.isAppWindowInvisible(component)
         }
     }
 
     /**
-     * Checks if no app windows containing the [partialWindowTitles] overlap with each other.
+     * Checks if no app windows containing the [component] overlap with each other.
      *
-     * @param partialWindowTitles partial titles of windows to check
+     * @param component Component to search
      */
-    fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply {
-        val repr = partialWindowTitles.joinToString(", ")
-        require(partialWindowTitles.size > 1) {
-            "Must give more than one window to check! (Given $repr)"
-        }
+    fun noWindowsOverlap(
+        vararg component: FlickerComponentName
+    ): WindowManagerTraceSubject = apply {
+        val repr = component.joinToString(", ") { it.toWindowName() }
+        verify("Must give more than one window to check! (Given $repr)")
+                .that(component)
+                .hasLength(1)
         addAssertion("noWindowsOverlap($repr)") {
-            it.noWindowsOverlap(*partialWindowTitles)
+            it.doNotOverlap(*component)
         }
     }
 
     /**
-     * Checks if the window named [aboveWindowTitle] is above the one named [belowWindowTitle] in
+     * Checks if the window named [aboveWindow] is above the one named [belowWindow] in
      * z-order.
      *
-     * @param aboveWindowTitle partial name of the expected top window
-     * @param belowWindowTitle partial name of the expected bottom window
+     * @param aboveWindow Expected top window
+     * @param belowWindow Expected bottom window
      */
     fun isAboveWindow(
-        aboveWindowTitle: String,
-        belowWindowTitle: String
+        aboveWindow: FlickerComponentName,
+        belowWindow: FlickerComponentName
     ): WindowManagerTraceSubject = apply {
+        val aboveWindowTitle = aboveWindow.toWindowName()
+        val belowWindowTitle = belowWindow.toWindowName()
         require(aboveWindowTitle != belowWindowTitle)
         addAssertion("$aboveWindowTitle is above $belowWindowTitle") {
-            it.isAboveWindow(aboveWindowTitle, belowWindowTitle)
+            it.isAboveWindow(aboveWindow, belowWindow)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the
-     * window's bounds cover each point in the region.
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at least
+     * [testRegion], that is, if its area of the window's bounds cover each point in the region.
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRegion Expected visible area of the window
      */
     fun coversAtLeast(
         testRegion: Region,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") {
-            it.frameRegion(partialWindowTitle).coversAtLeast(testRegion)
+        addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRegion)") {
+            it.frameRegion(component).coversAtLeast(testRegion)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the
-     * window's bounds cover each point in the region.
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at least
+     * [testRegion], that is, if its area of the window's bounds cover each point in the region.
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRegion Expected visible area of the window
      */
     fun coversAtLeast(
         testRegion: android.graphics.Region,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") {
-            it.frameRegion(partialWindowTitle).coversAtLeast(testRegion)
+        addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRegion)") {
+            it.frameRegion(component).coversAtLeast(testRegion)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the
-     * window's bounds cover each point in the region.
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at least
+     * [testRect], that is, if its area of the window's bounds cover each point in the region.
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversAtLeast(
         testRect: android.graphics.Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversAtLeast(testRect)
+        addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversAtLeast(testRect)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the
-     * window's bounds cover each point in the region.
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at least
+     * [testRect], that is, if its area of the window's bounds cover each point in the region.
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversAtLeast(
         testRect: Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversAtLeast(testRect)
+        addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversAtLeast(testRect)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the
-     * window state bounds don't cover any point outside of [testRegion].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at most
+     * [testRegion], that is, if the area of the window state bounds don't cover any point outside
+     * of [testRegion].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRegion Expected visible area of the window
      */
     fun coversAtMost(
         testRegion: Region,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") {
-            it.frameRegion(partialWindowTitle).coversAtMost(testRegion)
+        addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRegion)") {
+            it.frameRegion(component).coversAtMost(testRegion)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the
-     * window state bounds don't cover any point outside of [testRegion].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at most
+     * [testRegion], that is, if the area of the window state bounds don't cover any point outside
+     * of [testRegion].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRegion Expected visible area of the window
      */
     fun coversAtMost(
         testRegion: android.graphics.Region,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") {
-            it.frameRegion(partialWindowTitle).coversAtMost(testRegion)
+        addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRegion)") {
+            it.frameRegion(component).coversAtMost(testRegion)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the
-     * window state bounds don't cover any point outside of [testRect].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at most
+     * [testRect], that is, if the area of the window state bounds don't cover any point outside
+     * of [testRect].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversAtMost(
         testRect: Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversAtMost(testRect)
+        addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversAtMost(testRect)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the
-     * window state bounds don't cover any point outside of [testRect].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers at most
+     * [testRect], that is, if the area of the window state bounds don't cover any point outside
+     * of [testRect].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversAtMost(
         testRect: android.graphics.Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversAtMost(testRect)
+        addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversAtMost(testRect)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers exactly [testRegion].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly
+     * [testRegion].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRegion Expected visible area of the window
      */
     fun coversExactly(
         testRegion: android.graphics.Region,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversExactly($partialWindowTitle, $testRegion)") {
-            it.frameRegion(partialWindowTitle).coversExactly(testRegion)
+        addAssertion("coversExactly(${component?.toWindowName()}, $testRegion)") {
+            it.frameRegion(component).coversExactly(testRegion)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers exactly [testRect].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly
+     * [testRegion].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversExactly(
         testRect: Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversExactly($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversExactly(testRect)
+        addAssertion("coversExactly(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversExactly(testRect)
         }
     }
 
     /**
-     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
-     * containing [partialWindowTitle] covers exactly [testRect].
+     * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly
+     * [testRect].
      *
-     * @param partialWindowTitle Name of the layer to search
+     * @param component Component to search
      * @param testRect Expected visible area of the window
      */
     fun coversExactly(
         testRect: android.graphics.Rect,
-        partialWindowTitle: String
+        component: FlickerComponentName?
     ): WindowManagerTraceSubject = apply {
-        addAssertion("coversExactly($partialWindowTitle, $testRect)") {
-            it.frameRegion(partialWindowTitle).coversExactly(testRect)
+        addAssertion("coversExactly(${component?.toWindowName()}, $testRect)") {
+            it.frameRegion(component).coversExactly(testRect)
         }
     }
 
     /**
      * Checks that all visible layers are shown for more than one consecutive entry
      */
+    @JvmOverloads
     fun visibleWindowsShownMoreThanOneConsecutiveEntry(
-        ignoreWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
-            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+        ignoreWindows: List<FlickerComponentName> = listOf(
+            FlickerComponentName.SPLASH_SCREEN,
+            FlickerComponentName.SNAPSHOT)
     ): WindowManagerTraceSubject = apply {
         visibleEntriesShownMoreThanOneConsecutiveTime { subject ->
             subject.wmState.windowStates
                 .filter { it.isVisible }
                 .filter {
-                    ignoreWindows.none { windowName -> windowName in it.title }
+                    ignoreWindows.none { windowName -> windowName.toWindowName() in it.title }
                 }
                 .map { it.name }
                 .toSet()
@@ -463,8 +536,9 @@
      */
     operator fun invoke(
         name: String,
+        isOptional: Boolean = false,
         assertion: Assertion<WindowManagerStateSubject>
-    ): WindowManagerTraceSubject = apply { addAssertion(name, assertion) }
+    ): WindowManagerTraceSubject = apply { addAssertion(name, isOptional, assertion) }
 
     /**
      * Run the assertions for all trace entries within the specified time range
@@ -486,8 +560,10 @@
         /**
          * Boiler-plate Subject.Factory for WmTraceSubject
          */
-        private val FACTORY: Factory<Subject, WindowManagerTrace> =
-            Factory { fm, subject -> WindowManagerTraceSubject(fm, subject) }
+        private fun getFactory(
+            parent: WindowManagerTraceSubject?
+        ): Factory<Subject, WindowManagerTrace> =
+            Factory { fm, subject -> WindowManagerTraceSubject(fm, subject, parent) }
 
         /**
          * Creates a [WindowManagerTraceSubject] representing a WindowManager trace,
@@ -496,10 +572,14 @@
          * @param trace WindowManager trace
          */
         @JvmStatic
-        fun assertThat(trace: WindowManagerTrace): WindowManagerTraceSubject {
+        @JvmOverloads
+        fun assertThat(
+            trace: WindowManagerTrace,
+            parent: WindowManagerTraceSubject? = null
+        ): WindowManagerTraceSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(FACTORY)
+                .about(getFactory(parent))
                 .that(trace) as WindowManagerTraceSubject
             strategy.init(subject)
             return subject
@@ -509,6 +589,8 @@
          * Static method for getting the subject factory (for use with assertAbout())
          */
         @JvmStatic
-        fun entries(): Factory<Subject, WindowManagerTrace> = FACTORY
+        fun entries(
+            parent: WindowManagerTraceSubject?
+        ): Factory<Subject, WindowManagerTrace> = getFactory(parent)
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt
index 3666d03..41a3119 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt
@@ -21,6 +21,7 @@
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.RegionSubject
 import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.FailureStrategy
 import com.google.common.truth.StandardSubjectBuilder
@@ -45,8 +46,9 @@
  */
 class WindowStateSubject private constructor(
     fm: FailureMetadata,
+    override val parent: WindowManagerStateSubject?,
+    override val timestamp: Long,
     val windowState: WindowState?,
-    private val entry: WindowManagerStateSubject?,
     private val windowTitle: String? = null
 ) : FlickerSubject(fm, windowState) {
     val isEmpty: Boolean get() = windowState == null
@@ -54,10 +56,10 @@
     val isVisible: Boolean get() = windowState?.isVisible == true
     val isInvisible: Boolean get() = windowState?.isVisible == false
     val name: String get() = windowState?.name ?: windowTitle ?: ""
-    val frame: RegionSubject get() = RegionSubject.assertThat(windowState?.frame, listOf(this))
+    val frame: RegionSubject get() = RegionSubject.assertThat(windowState?.frame, this)
 
-    override val defaultFacts: String =
-        "${entry?.defaultFacts ?: ""}\nWindowTitle: ${windowState?.title}"
+    override val selfFacts = listOf(
+        Fact.fact("Window title", "${windowState?.title ?: windowTitle}"))
 
     /**
      * If the [windowState] exists, executes a custom [assertion] on the current subject
@@ -69,7 +71,7 @@
 
     /** {@inheritDoc} */
     override fun clone(): FlickerSubject {
-        return WindowStateSubject(fm, windowState, entry, windowTitle)
+        return WindowStateSubject(fm, parent, timestamp, windowState, windowTitle)
     }
 
     /**
@@ -95,10 +97,9 @@
          * Boiler-plate Subject.Factory for LayerSubject
          */
         @JvmStatic
-        @JvmOverloads
-        fun getFactory(entry: WindowManagerStateSubject? = null) =
+        fun getFactory(parent: WindowManagerStateSubject?, timestamp: Long, name: String?) =
             Factory { fm: FailureMetadata, subject: WindowState? ->
-                WindowStateSubject(fm, subject, entry)
+                WindowStateSubject(fm, parent, timestamp, subject, name)
             }
 
         /**
@@ -107,13 +108,14 @@
         @JvmStatic
         @JvmOverloads
         fun assertThat(
-            layer: WindowState?,
-            entry: WindowManagerStateSubject? = null
+            state: WindowState?,
+            parent: WindowManagerStateSubject? = null,
+            timestamp: Long
         ): WindowStateSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(entry))
-                .that(layer) as WindowStateSubject
+                .about(getFactory(parent, timestamp, name = null))
+                .that(state) as WindowStateSubject
             strategy.init(subject)
             return subject
         }
@@ -124,23 +126,15 @@
         @JvmStatic
         internal fun assertThat(
             name: String,
-            entry: WindowManagerStateSubject?
+            parent: WindowManagerStateSubject?,
+            timestamp: Long
         ): WindowStateSubject {
             val strategy = FlickerFailureStrategy()
             val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
-                .about(getFactory(entry, name))
+                .about(getFactory(parent, timestamp, name))
                 .that(null) as WindowStateSubject
             strategy.init(subject)
             return subject
         }
-
-        /**
-         * Boiler-plate Subject.Factory for LayerSubject
-         */
-        @JvmStatic
-        internal fun getFactory(entry: WindowManagerStateSubject?, name: String) =
-            Factory { fm: FailureMetadata, subject: WindowState? ->
-                WindowStateSubject(fm, subject, entry, name)
-            }
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/proto/errors.proto b/libraries/flicker/src/com/android/server/wm/proto/errors.proto
new file mode 100644
index 0000000..94427bb
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/proto/errors.proto
@@ -0,0 +1,35 @@
+syntax = "proto2";
+
+package com.android.server.wm.flicker;
+
+// Each message has its own class file created.
+option java_multiple_files = true;
+
+message FlickerErrorProto {
+  required string stacktrace = 1;
+  required string message = 2;
+  optional int32 layerId = 3;
+  optional string windowToken = 4;
+  optional int32 taskId = 5;
+  optional string assertionName = 6;
+}
+
+message FlickerErrorStateProto {
+  required int64 timestamp = 1;
+  repeated FlickerErrorProto errors = 2;
+}
+
+message FlickerErrorTraceProto {
+  /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 |
+     MagicNumber.MAGIC_NUMBER_L (this is needed because enums have to be 32 bits
+     and there's no nice way to put 64bit constants into .proto files. */
+  enum MagicNumber {
+    INVALID = 0;
+    MAGIC_NUMBER_L = 0x54525245; /* ERRT (little-endian ASCII) */
+    MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+  }
+
+  /* Must be the first field, set to value in MagicNumber */
+  optional fixed64 magic_number = 1;
+  repeated FlickerErrorStateProto states = 2;
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/proto/tags.proto b/libraries/flicker/src/com/android/server/wm/proto/tags.proto
new file mode 100644
index 0000000..dd352b5
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/proto/tags.proto
@@ -0,0 +1,45 @@
+syntax = "proto2";
+
+package com.android.server.wm.flicker;
+
+// Each message has its own class file created.
+option java_multiple_files = true;
+
+message FlickerTagProto {
+  enum Transition {
+    ROTATION = 0;
+    APP_LAUNCH = 1;
+    APP_CLOSE = 2;
+    IME_APPEAR = 3;
+    IME_DISAPPEAR = 4;
+    PIP_ENTER = 5;
+    PIP_RESIZE = 6;
+    PIP_EXIT = 7;
+  };
+  required bool isStartTag = 1;
+  required Transition transition = 2;
+  required int32 id = 3;
+  optional int32 layerId = 4;
+  optional string windowToken = 5;
+  optional int32 taskId = 6;
+}
+
+message FlickerTagStateProto {
+  required int64 timestamp = 1;
+  repeated FlickerTagProto tags = 2;
+}
+
+message FlickerTagTraceProto {
+  /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 |
+     MagicNumber.MAGIC_NUMBER_L (this is needed because enums have to be 32 bits
+     and there's no nice way to put 64bit constants into .proto files. */
+  enum MagicNumber {
+    INVALID = 0;
+    MAGIC_NUMBER_L = 0x54474154; /* TAGT (little-endian ASCII) */
+    MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+  }
+
+  /* Must be the first field, set to value in MagicNumber */
+  optional fixed64 magic_number = 1;
+  repeated FlickerTagStateProto states = 2;
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt
index 25e221e..46a6227 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt
@@ -16,7 +16,7 @@
 
 package com.android.server.wm.traces.common
 
-class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Bounds(width, height) {
+class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Size(width, height) {
     override fun prettyPrint(): String = prettyPrint(this)
 
     override fun equals(other: Any?): Boolean =
@@ -34,6 +34,8 @@
         return result
     }
 
+    override fun toString(): String = prettyPrint()
+
     companion object {
         val EMPTY: Buffer = Buffer(0, 0, 0, 0)
 
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt
index a4334fe..785977e 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt
@@ -27,6 +27,26 @@
 
     override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Color) return false
+
+        if (r != other.r) return false
+        if (g != other.g) return false
+        if (b != other.b) return false
+        if (a != other.a) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = r.hashCode()
+        result = 31 * result + g.hashCode()
+        result = 31 * result + b.hashCode()
+        result = 31 * result + a.hashCode()
+        return result
+    }
+
     companion object {
         val EMPTY = Color(r = -1f, g = -1f, b = -1f, a = 0f)
 
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt
new file mode 100644
index 0000000..3645304
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+/**
+ * The utility class to wait a condition with customized options.
+ * The default retry policy is 5 times with interval 1 second.
+ *
+ * @param <T> The type of the object to validate.
+ *
+ * <p>Sample:</p>
+ * <pre>
+ * // Simple case.
+ * if (Condition.waitFor("true value", () -> true)) {
+ *     println("Success");
+ * }
+ * // Wait for customized result with customized validation.
+ * String result = Condition.waitForResult(new Condition<String>("string comparison")
+ *         .setResultSupplier(() -> "Result string")
+ *         .setResultValidator(str -> str.equals("Expected string"))
+ *         .setRetryIntervalMs(500)
+ *         .setRetryLimit(3)
+ *         .setOnFailure(str -> println("Failed on " + str)));
+ * </pre>
+
+ * @param message The message to show what is waiting for.
+ * @param condition If it returns true, that means the condition is satisfied.
+ */
+open class Condition<T>(
+    protected open val message: String = "",
+    protected open val condition: (T) -> Boolean
+) {
+    /**
+     * @return if [value] satisfies the condition
+     */
+    fun isSatisfied(value: T): Boolean {
+        return condition.invoke(value)
+    }
+
+    /**
+     * @return the negation of the current assertion
+     */
+    fun negate(): Condition<T> = Condition(
+        message = "!$message") {
+        !this.condition.invoke(it)
+    }
+
+    /**
+     * @return a formatted message for the passing or failing condition on a state
+     */
+    open fun getMessage(value: T): String = "$message(passed=${isSatisfied(value)})"
+
+    override fun toString(): String = this.message
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt
new file mode 100644
index 0000000..acc6a5f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+/**
+ * The utility class to validate a set of conditions
+ *
+ * This class is used to easily integrate multiple conditions into a single
+ * verification, for example, during [WaitCondition], while keeping the individual
+ * conditions separate for better reuse
+ *
+ * @param conditions conditions to be checked
+ */
+class ConditionList<T>(
+    val conditions: List<Condition<T>>
+) : Condition<T>("", { false }) {
+    override val message: String
+        get() = conditions.joinToString(" and ") { it.toString() }
+
+    override val condition: (T) -> Boolean
+        get() = {
+            conditions.all { condition -> condition.isSatisfied(it) }
+        }
+
+    override fun getMessage(value: T): String = conditions
+        .joinToString(" and ") { it.getMessage(value) }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt
new file mode 100644
index 0000000..c96cd6c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * Represents a state dump containing the [WindowManagerState] and the [LayerTraceEntry] both
+ * parsed and in raw (byte) data.
+ */
+class DeviceStateDump<WMType : WindowManagerState?, LayerType : LayerTraceEntry?>(
+    /**
+     * Parsed [WindowManagerState]
+     */
+    val wmState: WMType,
+    /**
+     * Parsed [LayerTraceEntry]
+     */
+    val layerState: LayerType
+)
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt
new file mode 100644
index 0000000..f856a44
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Represents a state dump containing the [WindowManagerTrace] and the [LayersTrace] both parsed
+ * and in raw (byte) data.
+ */
+class DeviceTraceDump(
+    /**
+     * Parsed [WindowManagerTrace]
+     */
+    val wmTrace: WindowManagerTrace?,
+    /**
+     * Parsed [LayersTrace]
+     */
+    val layersTrace: LayersTrace?
+)
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt
index 1b24bae..efffd11 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt
@@ -22,7 +22,7 @@
 private const val HOUR_AS_NANOSECONDS: Long = 3600000000000
 private const val DAY_AS_NANOSECONDS: Long = 86400000000000
 
-internal fun prettyTimestamp(timestampNs: Long): String {
+fun prettyTimestamp(timestampNs: Long): String {
     // Necessary for compatibility with JS Number
     var remainingNs = "$timestampNs".toLong()
     val prettyTimestamp = StringBuilder()
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt b/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt
new file mode 100644
index 0000000..c39a3bf
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+/**
+ * Create a new component identifier.
+ *
+ * This is a version of Android's ComponentName class for flicker. This is necessary because
+ * flicker codebase it also compiled into KotlinJS for use into Winscope
+ *
+ * @param packageName The name of the package that the component exists in.  Can
+ * not be null.
+ * @param className The name of the class inside of <var>pkg</var> that
+ * implements the component.  Can not be null.
+ */
+data class FlickerComponentName(
+    val packageName: String,
+    val className: String
+) {
+    /**
+     * Obtains the activity name from the component name.
+     *
+     * See [ComponentName.toWindowName] for additional information
+     */
+    fun toActivityName(): String {
+        return when {
+            packageName.isNotEmpty() && className.isNotEmpty() -> {
+                val sb = StringBuilder(packageName.length + className.length)
+                appendShortString(sb, packageName, className)
+                return sb.toString()
+            }
+            packageName.isNotEmpty() -> packageName
+            className.isNotEmpty() -> className
+            else -> error("Component name should have an activity of class name")
+        }
+    }
+
+    /**
+     * Obtains the window name from the component name.
+     *
+     * [ComponentName] builds the string representation as PKG/CLASS, however this doesn't
+     * work for system components such as IME, NavBar and StatusBar, Toast.
+     *
+     * If the component doesn't have a package name, assume it's a system component and return only
+     * the class name
+     */
+    fun toWindowName(): String {
+        return when {
+            packageName.isNotEmpty() && className.isNotEmpty() -> "$packageName/$className"
+            packageName.isNotEmpty() -> packageName
+            className.isNotEmpty() -> className
+            else -> error("Component name should have an activity of class name")
+        }
+    }
+
+    /**
+     * Obtains the layer name from the component name.
+     *
+     * See [toWindowName] for additional information
+     */
+    fun toLayerName(): String {
+        var result = this.toWindowName()
+        if (result.contains("/") && !result.contains("#")) {
+            result = "$result#"
+        }
+
+        return result
+    }
+
+    private fun appendShortString(sb: StringBuilder, packageName: String, className: String) {
+        sb.append(packageName).append('/')
+        appendShortClassName(sb, packageName, className)
+    }
+
+    private fun appendShortClassName(sb: StringBuilder, packageName: String, className: String) {
+        if (className.startsWith(packageName)) {
+            val packageNameLength = packageName.length
+            val classNameLength = className.length
+            if (classNameLength > packageNameLength && className[packageNameLength] == '.') {
+                sb.append(className, packageNameLength, classNameLength)
+                return
+            }
+        }
+        sb.append(className)
+    }
+
+    companion object {
+        val NAV_BAR = FlickerComponentName("", "NavigationBar0")
+        val STATUS_BAR = FlickerComponentName("", "StatusBar")
+        val ROTATION = FlickerComponentName("", "RotationLayer")
+        val BACK_SURFACE = FlickerComponentName("", "BackColorSurface")
+        val IME = FlickerComponentName("", "InputMethod")
+        val SPLASH_SCREEN = FlickerComponentName("", "Splash Screen")
+        val SNAPSHOT = FlickerComponentName("", "SnapshotStartingWindow")
+        val WALLPAPER_BBQ_WRAPPER =
+                FlickerComponentName("", "Wallpaper BBQ wrapper")
+
+        fun unflattenFromString(str: String): FlickerComponentName {
+            val sep = str.indexOf('/')
+            if (sep < 0 || sep + 1 >= str.length) {
+                error("Missing package/class separator")
+            }
+            val pkg = str.substring(0, sep)
+            var cls = str.substring(sep + 1)
+            if (cls.isNotEmpty() && cls[0] == '.') {
+                cls = pkg + cls
+            }
+            return FlickerComponentName(pkg, cls)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt
index 6dc532d..75af785 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt
@@ -17,9 +17,8 @@
 package com.android.server.wm.traces.common
 
 interface ITrace<Entry : ITraceEntry> {
-    val entries: List<Entry>
+    val entries: Array<Entry>
     val source: String
-    val sourceChecksum: String
 
     fun getEntry(timestamp: Long): Entry {
         return entries.firstOrNull { it.timestamp == timestamp }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt
index 3b3c2b2..84b7e2c 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt
@@ -21,6 +21,22 @@
 
     override fun toString(): String = prettyPrint()
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Point) return false
+
+        if (x != other.x) return false
+        if (y != other.y) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = x
+        result = 31 * result + y
+        return result
+    }
+
     companion object {
         fun prettyPrint(point: Point): String = "(${point.x}, ${point.y})"
     }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt
index db55eda..5830ad0 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt
@@ -73,11 +73,11 @@
      * @return A rectangle with the intersection coordinates
      */
     fun intersection(left: Float, top: Float, right: Float, bottom: Float): RectF {
-        if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) {
-            var intersectionLeft = 0f
-            var intersectionTop = 0f
-            var intersectionRight = 0f
-            var intersectionBottom = 0f
+        if (this.left < right && left < this.right && this.top <= bottom && top <= this.bottom) {
+            var intersectionLeft = this.left
+            var intersectionTop = this.top
+            var intersectionRight = this.right
+            var intersectionBottom = this.bottom
 
             if (this.left < left) {
                 intersectionLeft = left
@@ -111,6 +111,26 @@
 
     override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is RectF) return false
+
+        if (left != other.left) return false
+        if (top != other.top) return false
+        if (right != other.right) return false
+        if (bottom != other.bottom) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = left.hashCode()
+        result = 31 * result + top.hashCode()
+        result = 31 * result + right.hashCode()
+        result = 31 * result + bottom.hashCode()
+        return result
+    }
+
     companion object {
         val EMPTY = RectF()
 
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt
index 0bacfe9..b6c1734 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt
@@ -39,6 +39,22 @@
 
     override fun prettyPrint(): String = rects.joinToString(", ") { it.prettyPrint() }
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Region) return false
+        if (!super.equals(other)) return false
+
+        if (!rects.contentEquals(other.rects)) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + rects.contentHashCode()
+        return result
+    }
+
     companion object {
         val EMPTY = Region()
     }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Size.kt
similarity index 81%
rename from libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt
rename to libraries/flicker/src/com/android/server/wm/traces/common/Size.kt
index 52b8384..d5bd20a 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Size.kt
@@ -16,22 +16,19 @@
 
 package com.android.server.wm.traces.common
 
-open class Bounds(val width: Int, val height: Int) {
+open class Size(val width: Int, val height: Int) {
     open val isEmpty: Boolean
         get() = height == 0 || width == 0
 
     val isNotEmpty: Boolean
         get() = !isEmpty
 
-    val size: Bounds
-        get() = Bounds(width, height)
-
     open fun prettyPrint(): String = prettyPrint(this)
 
     override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
 
     override fun equals(other: Any?): Boolean =
-        other is Bounds &&
+        other is Size &&
             other.height == height &&
             other.width == width
 
@@ -42,8 +39,8 @@
     }
 
     companion object {
-        val EMPTY: Bounds = Bounds(0, 0)
+        val EMPTY: Size = Size(0, 0)
 
-        fun prettyPrint(bounds: Bounds): String = "${bounds.width} x ${bounds.height}"
+        fun prettyPrint(bounds: Size): String = "${bounds.width} x ${bounds.height}"
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt
new file mode 100644
index 0000000..cd8e316
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+/**
+ * The utility class to wait a condition with customized options.
+ * The default retry policy is 5 times with interval 1 second.
+ *
+ * @param <T> The type of the object to validate.
+ *
+ * <p>Sample:</p>
+ * <pre>
+ * // Simple case.
+ * if (Condition.waitFor("true value", () -> true)) {
+ *     println("Success");
+ * }
+ * // Wait for customized result with customized validation.
+ * String result = WaitForCondition.Builder(supplier = () -> "Result string")
+ *         .withCondition(str -> str.equals("Expected string"))
+ *         .withRetryIntervalMs(500)
+ *         .withRetryLimit(3)
+ *         .onFailure(str -> println("Failed on " + str)))
+ *         .build()
+ *         .waitFor()
+ * </pre>
+
+ * @param condition If it returns true, that means the condition is satisfied.
+ */
+class WaitCondition<T> private constructor(
+    private val supplier: () -> T,
+    private val condition: Condition<T>,
+    private val retryLimit: Int,
+    private val onLog: ((String) -> Unit)?,
+    private val onFailure: ((T) -> Any)?,
+    private val onRetry: ((T) -> Any)?,
+    private val onSuccess: ((T) -> Any)?
+) {
+    /**
+     * @return `false` if the condition does not satisfy within the time limit.
+     */
+    fun waitFor(): Boolean {
+        onLog?.invoke("***Waiting for $condition")
+        var currState: T? = null
+        for (i in 0..retryLimit) {
+            currState = supplier.invoke()
+            if (condition.isSatisfied(currState)) {
+                onLog?.invoke("***Waiting for $condition ... Success!")
+                onSuccess?.invoke(currState)
+                return true
+            } else {
+                val detailedMessage = condition.getMessage(currState)
+                onLog?.invoke("***Waiting for $detailedMessage... retry=${i + 1}")
+                if (i < retryLimit) {
+                    onRetry?.invoke(currState)
+                }
+            }
+        }
+
+        val detailedMessage = if (currState != null) {
+            condition.getMessage(currState)
+        } else {
+            condition.toString()
+        }
+        onLog?.invoke("***Waiting for $detailedMessage ... Failed!")
+        if (onFailure != null) {
+            require(currState != null) { "Missing last result for failure notification" }
+            onFailure.invoke(currState)
+        }
+        return false
+    }
+
+    class Builder<T>(
+        private val supplier: () -> T,
+        private var retryLimit: Int
+    ) {
+        private val conditions = mutableListOf<Condition<T>>()
+        private var onFailure: ((T) -> Any)? = null
+        private var onRetry: ((T) -> Any)? = null
+        private var onSuccess: ((T) -> Any)? = null
+        private var onLog: ((String) -> Unit)? = null
+
+        fun withCondition(condition: Condition<T>) =
+            apply { conditions.add(condition) }
+
+        fun withCondition(message: String, condition: (T) -> Boolean) =
+            apply { withCondition(Condition(message, condition)) }
+
+        private fun spreadConditionList(): List<Condition<T>> =
+            conditions.flatMap {
+                if (it is ConditionList<T>) {
+                    it.conditions
+                } else {
+                    listOf(it)
+                }
+            }
+
+        /**
+         * Executes the action when the condition does not satisfy within the time limit. The passed
+         * object to the consumer will be the last result from the supplier.
+         */
+        fun onFailure(onFailure: (T) -> Any): Builder<T> =
+            apply { this.onFailure = onFailure }
+
+        fun onLog(onLog: (String) -> Unit): Builder<T> =
+            apply { this.onLog = onLog }
+
+        fun onRetry(onRetry: ((T) -> Any)? = null): Builder<T> =
+            apply { this.onRetry = onRetry }
+
+        fun onSuccess(onRetry: ((T) -> Any)? = null): Builder<T> =
+            apply { this.onSuccess = onRetry }
+
+        fun build(): WaitCondition<T> =
+            WaitCondition(supplier, ConditionList(spreadConditionList()), retryLimit,
+                onLog, onFailure, onRetry, onSuccess)
+    }
+
+    companion object {
+        // TODO(b/112837428): Implement a incremental retry policy to reduce the unnecessary
+        // constant time, currently keep the default as 5*1s because most of the original code
+        // uses it, and some tests might be sensitive to the waiting interval.
+        const val DEFAULT_RETRY_LIMIT = 10
+        const val DEFAULT_RETRY_INTERVAL_MS = 500L
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt b/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt
new file mode 100644
index 0000000..030ad18
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common
+
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+
+object WindowManagerConditionsFactory {
+    private val navBarWindowName = FlickerComponentName.NAV_BAR.toWindowName()
+    private val navBarLayerName = FlickerComponentName.NAV_BAR.toLayerName()
+    private val statusBarWindowName = FlickerComponentName.STATUS_BAR.toWindowName()
+    private val statusBarLayerName = FlickerComponentName.STATUS_BAR.toLayerName()
+
+    /**
+     * Condition to check if the nav bar window is visible
+     */
+    fun isNavBarVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        ConditionList(listOf(
+            isNavBarWindowVisible(), isNavBarLayerVisible(), isNavBarLayerOpaque()))
+
+    /**
+     * Condition to check if the nav bar window is visible
+     */
+    fun isNavBarWindowVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isNavBarWindowVisible") {
+            it.wmState.isWindowVisible(navBarWindowName)
+        }
+
+    /**
+     * Condition to check if the nav bar layer is visible
+     */
+    fun isNavBarLayerVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        isLayerVisible(navBarLayerName)
+
+    /**
+     * Condition to check if the nav bar layer is opaque
+     */
+    fun isNavBarLayerOpaque(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isNavBarLayerOpaque") {
+            it.layerState.getLayerWithBuffer(navBarLayerName)
+                ?.color?.a ?: 0f == 1f
+        }
+
+    /**
+     * Condition to check if the status bar window is visible
+     */
+    fun isStatusBarVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        ConditionList(listOf(
+            isStatusBarWindowVisible(), isStatusBarLayerVisible(), isStatusBarLayerOpaque()))
+
+    /**
+     * Condition to check if the nav bar window is visible
+     */
+    fun isStatusBarWindowVisible():
+        Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isStatusBarWindowVisible") {
+            it.wmState.isWindowVisible(statusBarWindowName)
+        }
+
+    /**
+     * Condition to check if the nav bar layer is visible
+     */
+    fun isStatusBarLayerVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        isLayerVisible(statusBarLayerName)
+
+    /**
+     * Condition to check if the nav bar layer is opaque
+     */
+    fun isStatusBarLayerOpaque(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isStatusBarLayerOpaque") {
+            it.layerState.getLayerWithBuffer(statusBarLayerName)
+                ?.color?.a ?: 0f == 1f
+        }
+
+    fun isHomeActivityVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isHomeActivityVisible") {
+            it.wmState.homeActivity?.isVisible == true
+        }
+
+    fun isAppTransitionIdle(
+        displayId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isAppTransitionIdle[$displayId]") {
+            it.wmState.getDisplay(displayId)
+                ?.appTransitionState == WindowManagerState.APP_STATE_IDLE
+        }
+
+    fun containsActivity(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("containsActivity[${component.toActivityName()}]") {
+            it.wmState.containsActivity(component.toActivityName())
+        }
+
+    fun containsWindow(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("containsWindow[${component.toWindowName()}]") {
+            it.wmState.containsWindow(component.toWindowName())
+        }
+
+    fun isWindowSurfaceShown(
+        windowName: String
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isWindowSurfaceShown[$windowName]") {
+            it.wmState.isWindowSurfaceShown(windowName)
+        }
+
+    fun isWindowSurfaceShown(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        isWindowSurfaceShown(component.toWindowName())
+
+    fun isActivityVisible(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isActivityVisible") {
+            it.wmState.isActivityVisible(component.toActivityName())
+        }
+
+    fun isWMStateComplete(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isWMStateComplete") {
+            it.wmState.isComplete()
+        }
+
+    fun hasRotation(
+        expectedRotation: Int,
+        displayId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> {
+        val hasRotationCondition = Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>>(
+            "hasRotation[$expectedRotation, display=$displayId]") {
+            val currRotation = it.wmState.getRotation(displayId)
+            currRotation == expectedRotation
+        }
+        return ConditionList(listOf(
+            hasRotationCondition,
+            isLayerVisible(FlickerComponentName.ROTATION).negate(),
+            isLayerVisible(FlickerComponentName.BACK_SURFACE).negate(),
+            hasLayersAnimating().negate()
+        ))
+    }
+
+    fun isLayerVisible(
+        layerName: String
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerVisible[$layerName]") {
+            it.layerState.isVisible(layerName)
+        }
+
+    fun isLayerVisible(
+        layerId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerVisible[$layerId]") {
+            it.layerState.getLayerById(layerId)?.isVisible ?: false
+        }
+
+    fun isLayerColorAlphaOne(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerColorAlphaOne[${component.toLayerName()}]") {
+            val layers = it.layerState.getVisibleLayersByName(component)
+            layers.any { layer -> layer.color.a == 1.0f }
+        }
+
+    fun isLayerColorAlphaOne(
+        layerId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerColorAlphaOne[$layerId]") {
+            val layer = it.layerState.getLayerById(layerId)
+            layer?.color?.a == 1.0f
+        }
+
+    fun isLayerTransformFlagSet(
+        component: FlickerComponentName,
+        transform: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerTransformFlagSet[" +
+            "${component.toLayerName()},transform=$transform]") {
+            val layers = it.layerState.getVisibleLayersByName(component)
+            layers.any { layer -> isTransformFlagSet(layer, transform) }
+        }
+
+    fun isLayerTransformFlagSet(
+        layerId: Int,
+        transform: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isLayerTransformFlagSet[$layerId, $transform]") {
+            val layer = it.layerState.getLayerById(layerId)
+            layer?.transform?.type?.isFlagSet(transform) ?: false
+        }
+
+    fun isLayerTransformIdentity(
+        layerId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        ConditionList(listOf(
+            isLayerTransformFlagSet(layerId, Transform.SCALE_VAL).negate(),
+            isLayerTransformFlagSet(layerId, Transform.TRANSLATE_VAL).negate(),
+            isLayerTransformFlagSet(layerId, Transform.ROTATE_VAL).negate()
+        ))
+
+    private fun isTransformFlagSet(layer: Layer, transform: Int): Boolean =
+        layer.transform.type?.isFlagSet(transform) ?: false
+
+    fun LayerTraceEntry.getVisibleLayersByName(
+        component: FlickerComponentName
+    ): List<Layer> = visibleLayers.filter { it.name.contains(component.toLayerName()) }
+
+    fun isLayerVisible(
+        component: FlickerComponentName
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        isLayerVisible(component.toLayerName())
+
+    fun hasLayersAnimating(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("hasLayersAnimating") {
+            it.layerState.isAnimating()
+        }
+
+    fun isPipWindowLayerSizeMatch(
+        layerId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isPipWindowLayerSizeMatch") {
+            val pipWindow = it.wmState.pinnedWindows.firstOrNull { it.layerId == layerId }
+                ?: error("Unable to find window with layerId $layerId")
+            val windowHeight = pipWindow.frame.height.toFloat()
+            val windowWidth = pipWindow.frame.width.toFloat()
+
+            val pipLayer = it.layerState.getLayerById(layerId)
+            val layerHeight = pipLayer?.sourceBounds?.height
+                ?: error("Unable to find layer with id $layerId")
+            val layerWidth = pipLayer.sourceBounds.width
+
+            windowHeight == layerHeight && windowWidth == layerWidth
+        }
+
+    fun hasPipWindow(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("hasPipWindow") {
+            it.wmState.hasPipWindow()
+        }
+
+    fun isImeShown(
+        displayId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        ConditionList(listOf(
+            isImeOnDisplay(displayId),
+            isLayerVisible(FlickerComponentName.IME),
+            isImeSurfaceShown(),
+            isWindowSurfaceShown(FlickerComponentName.IME.toWindowName())
+        ))
+
+    private fun isImeOnDisplay(
+        displayId: Int
+    ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isImeOnDisplay[$displayId]") {
+            it.wmState.inputMethodWindowState?.displayId == displayId
+        }
+
+    private fun isImeSurfaceShown():
+        Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("isImeSurfaceShown") {
+            it.wmState.inputMethodWindowState?.isSurfaceShown == true
+        }
+
+    fun isAppLaunchEnded(taskId: Int):
+        Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        Condition("containsVisibleAppLaunchWindow[$taskId]") { dump ->
+            val windowStates = dump.wmState.getRootTask(taskId)?.activities?.flatMap {
+                it.children.filterIsInstance<WindowState>()
+            }
+            windowStates != null && windowStates.none { window ->
+                window.attributes.type == PlatformConsts.TYPE_APPLICATION_STARTING &&
+                    window.isVisible
+            }
+        }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt
new file mode 100644
index 0000000..7663b93
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.errors
+
+/**
+ * Flicker Error identified in a WindowManager or SurfaceFlinger trace
+ * @param stacktrace Stacktrace to identify source of errors
+ * @param message Message to explain error briefly
+ * @param layerId The layer which the error is associated with
+ * @param windowToken The window which the error is associated with
+ * @param taskId The task which the error is associated with
+ * @param assertionName The class name of the assertion that generated the error
+ */
+data class Error(
+    val stacktrace: String,
+    val message: String,
+    val layerId: Int = 0,
+    val windowToken: String = "",
+    val taskId: Int = 0,
+    val assertionName: String = ""
+)
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt
new file mode 100644
index 0000000..0d285e0
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt
@@ -0,0 +1,33 @@
+package com.android.server.wm.traces.common.errors
+
+import com.android.server.wm.traces.common.ITraceEntry
+import com.android.server.wm.traces.common.prettyTimestamp
+
+/**
+ * A state at a particular time within a trace that holds a list of errors there may be.
+ * @param errors Errors contained in the state
+ * @param _timestamp Timestamp of this state
+ */
+class ErrorState(
+    val errors: Array<Error>,
+    _timestamp: String
+) : ITraceEntry {
+    override val timestamp: Long = _timestamp.toLong()
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ErrorState) return false
+        if (timestamp != other.timestamp) return false
+        if (errors.contentEquals(other.errors)) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = timestamp.hashCode()
+        result = 31 * result + errors.contentDeepHashCode()
+        return result
+    }
+
+    override fun toString(): String = "FlickerErrorState(" +
+            "timestamp=${prettyTimestamp(timestamp)}, numberOfErrors=${errors.size})"
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt
new file mode 100644
index 0000000..e851e1d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.errors
+
+import com.android.server.wm.traces.common.ITrace
+
+/**
+ * Represents all the states with errors in an entire trace.
+ * @param entries States with errors contained in this trace
+ * @param source Source of the trace
+ */
+data class ErrorTrace(
+    override val entries: Array<ErrorState>,
+    override val source: String
+) : ITrace<ErrorState>,
+    List<ErrorState> by entries.toList() {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ErrorTrace) return false
+        if (entries != other.entries) return false
+        return true
+    }
+
+    override fun hashCode(): Int = entries.contentDeepHashCode()
+
+    override fun toString(): String = "FlickerErrorTrace(First: ${entries.first()}," +
+            "End: ${entries.last()})"
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt
new file mode 100644
index 0000000..1dfa94e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.layers
+
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.Size
+
+/**
+ * Representation of a Display in the SF trace
+ */
+data class Display(
+    val id: ULong,
+    val name: String,
+    val layerStackId: Int,
+    val size: Size,
+    val layerStackSpace: Rect,
+    val transform: Transform
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Display) return false
+
+        if (id != other.id) return false
+        if (name != other.name) return false
+        if (layerStackId != other.layerStackId) return false
+        if (size != other.size) return false
+        if (layerStackSpace != other.layerStackSpace) return false
+        if (transform != other.transform) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = id.toInt()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + layerStackId
+        result = 31 * result + size.hashCode()
+        result = 31 * result + layerStackSpace.hashCode()
+        result = 31 * result + transform.hashCode()
+        return result
+    }
+
+    companion object {
+        val EMPTY = Display(
+            id = 0.toULong(),
+            name = "EMPTY",
+            layerStackId = -1,
+            size = Size.EMPTY,
+            layerStackSpace = Rect.EMPTY,
+            transform = Transform.EMPTY
+        )
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt
index 2dd7630..ddff3eb 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt
@@ -19,8 +19,9 @@
 import com.android.server.wm.traces.common.Buffer
 import com.android.server.wm.traces.common.Color
 import com.android.server.wm.traces.common.Rect
-import com.android.server.wm.traces.common.Region
 import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet
 
 /**
  * Represents a single layer with links to its parent and child layers.
@@ -29,7 +30,7 @@
  * access internal Java/Android functionality
  *
  **/
-open class Layer(
+data class Layer(
     val name: String,
     val id: Int,
     val parentId: Int,
@@ -37,15 +38,15 @@
     val visibleRegion: Region?,
     val activeBuffer: Buffer,
     val flags: Int,
-    _bounds: RectF?,
+    val bounds: RectF,
     val color: Color,
-    _isOpaque: Boolean,
+    private val _isOpaque: Boolean,
     val shadowRadius: Float,
     val cornerRadius: Float,
     val type: String,
-    _screenBounds: RectF?,
+    private val _screenBounds: RectF?,
     val transform: Transform,
-    _sourceBounds: RectF?,
+    val sourceBounds: RectF,
     val currFrame: Long,
     val effectiveScalingMode: Int,
     val bufferTransform: Transform,
@@ -57,7 +58,8 @@
     val isRelativeOf: Boolean,
     val zOrderRelativeOfId: Int
 ) {
-    lateinit var parent: Layer
+    val stableId: String = "$type $id $name"
+    var parent: Layer? = null
     var zOrderRelativeOf: Layer? = null
     var zOrderRelativeParentOf: Int = 0
 
@@ -66,22 +68,32 @@
      *
      * @return
      */
-    val isRootLayer: Boolean
-        get() {
-            return !::parent.isInitialized
-        }
+    val isRootLayer: Boolean get() = parent == null
 
-    val children = mutableListOf<Layer>()
-    val occludedBy = mutableListOf<Layer>()
-    val partiallyOccludedBy = mutableListOf<Layer>()
-    val coveredBy = mutableListOf<Layer>()
+    private val _children = mutableListOf<Layer>()
+    private val _occludedBy = mutableListOf<Layer>()
+    private val _partiallyOccludedBy = mutableListOf<Layer>()
+    private val _coveredBy = mutableListOf<Layer>()
+    val children: Array<Layer>
+        get() = _children.toTypedArray()
+    val occludedBy: Array<Layer>
+        get() = _occludedBy.toTypedArray()
+    val partiallyOccludedBy: Array<Layer>
+        get() = _partiallyOccludedBy.toTypedArray()
+    val coveredBy: Array<Layer>
+        get() = _coveredBy.toTypedArray()
+    var isMissing: Boolean = false
+        internal set
 
-    fun addChild(childLayer: Layer) {
-        children.add(childLayer)
-    }
+    val isScaling: Boolean
+        get() = isTransformFlagSet(Transform.SCALE_VAL)
+    val isTranslating: Boolean
+        get() = isTransformFlagSet(Transform.TRANSLATE_VAL)
+    val isRotating: Boolean
+        get() = isTransformFlagSet(Transform.ROTATE_VAL)
 
-    val bounds: RectF = _bounds ?: RectF.EMPTY
-    val sourceBounds: RectF = _sourceBounds ?: RectF.EMPTY
+    private fun isTransformFlagSet(transform: Int): Boolean =
+            this.transform.type?.isFlagSet(transform) ?: false
 
     /**
      * Checks if the layer's active buffer is empty
@@ -135,10 +147,7 @@
      *
      * @return
      */
-    val fillsColor: Boolean
-        get() {
-            return color.isNotEmpty
-        }
+    val fillsColor: Boolean get() = color.isNotEmpty
 
     /**
      * Checks if the [Layer] draws a shadow
@@ -209,19 +218,13 @@
     val isEffectLayer: Boolean get() = type == "EffectLayer"
 
     /**
-     * Checks if the [Layer] is not visible
-     *
-     * @return
-     */
-    val isInvisible: Boolean get() = !isVisible
-
-    /**
      * Checks if the [Layer] is hidden by its parent
      *
      * @return
      */
     val isHiddenByParent: Boolean
-        get() = !isRootLayer && (parent.isHiddenByPolicy || parent.isHiddenByParent)
+        get() = !isRootLayer &&
+            (parent?.isHiddenByPolicy == true || parent?.isHiddenByParent == true)
 
     /**
      * Gets a description of why the layer is (in)visible
@@ -234,7 +237,7 @@
                 isVisible -> ""
                 isContainerLayer -> "ContainerLayer"
                 isHiddenByPolicy -> "Flag is hidden"
-                isHiddenByParent -> "Hidden by parent ${parent.name}"
+                isHiddenByParent -> "Hidden by parent ${parent?.name}"
                 isBufferLayer && isActiveBufferEmpty -> "Buffer is empty"
                 color.isEmpty -> "Alpha is 0"
                 crop?.isEmpty ?: false -> "Crop is 0x0"
@@ -243,8 +246,8 @@
                 isRelativeOf && zOrderRelativeOf == null -> "RelativeOf layer has been removed"
                 isEffectLayer && !fillsColor && !drawsShadows && !hasBlur ->
                     "Effect layer does not have color fill, shadow or blur"
-                occludedBy.isNotEmpty() -> {
-                    val occludedByIds = occludedBy.joinToString(", ") { it.id.toString() }
+                _occludedBy.isNotEmpty() -> {
+                    val occludedByIds = _occludedBy.joinToString(", ") { it.id.toString() }
                     "Layer is occluded by: $occludedByIds"
                 }
                 visibleRegion?.isEmpty ?: false ->
@@ -259,6 +262,18 @@
         else -> transform.apply(bounds)
     }
 
+    val absoluteZ: String
+        get() {
+            val zOrderRelativeOf = zOrderRelativeOf
+            return buildString {
+                when {
+                    zOrderRelativeOf != null -> append(zOrderRelativeOf.absoluteZ).append(",")
+                    parent != null -> append(parent?.absoluteZ).append(",")
+                }
+                append(z)
+            }
+        }
+
     fun contains(innerLayer: Layer): Boolean {
         return if (!this.transform.isSimpleRotation || !innerLayer.transform.isSimpleRotation) {
             false
@@ -267,6 +282,22 @@
         }
     }
 
+    fun addChild(childLayer: Layer) {
+        _children.add(childLayer)
+    }
+
+    fun addOccludedBy(layers: Array<Layer>) {
+        _occludedBy.addAll(layers)
+    }
+
+    fun addPartiallyOccludedBy(layers: Array<Layer>) {
+        _partiallyOccludedBy.addAll(layers)
+    }
+
+    fun addCoveredBy(layers: Array<Layer>) {
+        _coveredBy.addAll(layers)
+    }
+
     fun overlaps(other: Layer): Boolean =
         !this.screenBounds.intersection(other.screenBounds).isEmpty
 
@@ -275,7 +306,7 @@
             append(name)
 
             if (activeBuffer.isNotEmpty) {
-                append(" buffer:${activeBuffer.width}x${activeBuffer.height}")
+                append(" buffer:$activeBuffer")
                 append(" frame#$currFrame")
             }
 
@@ -286,13 +317,42 @@
     }
 
     override fun equals(other: Any?): Boolean {
-        return other is Layer &&
-            other.parentId == this.parentId &&
-            other.name == this.name &&
-            other.flags == this.flags &&
-            other.currFrame == this.currFrame &&
-            other.activeBuffer == this.activeBuffer &&
-            other.screenBounds == this.screenBounds
+        if (this === other) return true
+        if (other !is Layer) return false
+
+        if (name != other.name) return false
+        if (id != other.id) return false
+        if (parentId != other.parentId) return false
+        if (z != other.z) return false
+        if (visibleRegion != other.visibleRegion) return false
+        if (activeBuffer != other.activeBuffer) return false
+        if (flags != other.flags) return false
+        if (bounds != other.bounds) return false
+        if (color != other.color) return false
+        if (shadowRadius != other.shadowRadius) return false
+        if (cornerRadius != other.cornerRadius) return false
+        if (type != other.type) return false
+        if (transform != other.transform) return false
+        if (sourceBounds != other.sourceBounds) return false
+        if (currFrame != other.currFrame) return false
+        if (effectiveScalingMode != other.effectiveScalingMode) return false
+        if (bufferTransform != other.bufferTransform) return false
+        if (hwcCompositionType != other.hwcCompositionType) return false
+        if (hwcCrop != other.hwcCrop) return false
+        if (hwcFrame != other.hwcFrame) return false
+        if (backgroundBlurRadius != other.backgroundBlurRadius) return false
+        if (crop != other.crop) return false
+        if (isRelativeOf != other.isRelativeOf) return false
+        if (zOrderRelativeOfId != other.zOrderRelativeOfId) return false
+        if (stableId != other.stableId) return false
+        if (parent != other.parent) return false
+        if (zOrderRelativeOf != other.zOrderRelativeOf) return false
+        if (zOrderRelativeParentOf != other.zOrderRelativeParentOf) return false
+        if (isMissing != other.isMissing) return false
+        if (isOpaque != other.isOpaque) return false
+        if (screenBounds != other.screenBounds) return false
+
+        return true
     }
 
     override fun hashCode(): Int {
@@ -300,26 +360,33 @@
         result = 31 * result + id
         result = 31 * result + parentId
         result = 31 * result + z
-        result = 31 * result + visibleRegion.hashCode()
+        result = 31 * result + (visibleRegion?.hashCode() ?: 0)
         result = 31 * result + activeBuffer.hashCode()
         result = 31 * result + flags
         result = 31 * result + bounds.hashCode()
         result = 31 * result + color.hashCode()
-        result = 31 * result + isOpaque.hashCode()
         result = 31 * result + shadowRadius.hashCode()
         result = 31 * result + cornerRadius.hashCode()
         result = 31 * result + type.hashCode()
-        result = 31 * result + screenBounds.hashCode()
         result = 31 * result + transform.hashCode()
         result = 31 * result + sourceBounds.hashCode()
         result = 31 * result + currFrame.hashCode()
         result = 31 * result + effectiveScalingMode
         result = 31 * result + bufferTransform.hashCode()
-        result = 31 * result + parent.hashCode()
-        result = 31 * result + children.hashCode()
-        result = 31 * result + occludedBy.hashCode()
-        result = 31 * result + partiallyOccludedBy.hashCode()
-        result = 31 * result + coveredBy.hashCode()
+        result = 31 * result + hwcCompositionType
+        result = 31 * result + hwcCrop.hashCode()
+        result = 31 * result + hwcFrame.hashCode()
+        result = 31 * result + backgroundBlurRadius
+        result = 31 * result + (crop?.hashCode() ?: 0)
+        result = 31 * result + isRelativeOf.hashCode()
+        result = 31 * result + zOrderRelativeOfId
+        result = 31 * result + stableId.hashCode()
+        result = 31 * result + (parent?.hashCode() ?: 0)
+        result = 31 * result + (zOrderRelativeOf?.hashCode() ?: 0)
+        result = 31 * result + zOrderRelativeParentOf
+        result = 31 * result + isMissing.hashCode()
+        result = 31 * result + isOpaque.hashCode()
+        result = 31 * result + screenBounds.hashCode()
         return result
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt
index c440de9..47ec4bf 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt
@@ -27,13 +27,18 @@
  *
  **/
 open class LayerTraceEntry constructor(
-    override val timestamp: Long, // hierarchical representation of layers
+    override val timestamp: Long,
     val hwcBlob: String,
     val where: String,
+    val displays: Array<Display>,
     _rootLayers: Array<Layer>
 ) : ITraceEntry {
+    val isVisible = true
+    val stableId: String get() = this::class.simpleName ?: error("Unable to determine class")
+    val name: String get() = prettyTimestamp(timestamp)
+
     val flattenedLayers: Array<Layer> = fillFlattenedLayers(_rootLayers)
-    val rootLayers: Array<Layer> get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray()
+    val children: Array<Layer> get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray()
 
     private fun fillFlattenedLayers(rootLayers: Array<Layer>): Array<Layer> {
         val opaqueLayers = mutableListOf<Layer>()
@@ -79,11 +84,15 @@
             val visible = layer.isVisible
 
             if (visible) {
-                layer.occludedBy.addAll(opaqueLayers
-                    .filter { it.contains(layer) && !it.hasRoundedCorners })
-                layer.partiallyOccludedBy.addAll(
-                    opaqueLayers.filter { it.overlaps(layer) && it !in layer.occludedBy })
-                layer.coveredBy.addAll(transparentLayers.filter { it.overlaps(layer) })
+                val occludedBy = opaqueLayers
+                        .filter { it.contains(layer) && !it.hasRoundedCorners }.toTypedArray()
+                layer.addOccludedBy(occludedBy)
+                val partiallyOccludedBy = opaqueLayers
+                        .filter { it.overlaps(layer) && it !in layer.occludedBy }
+                        .toTypedArray()
+                layer.addPartiallyOccludedBy(partiallyOccludedBy)
+                val coveredBy = transparentLayers.filter { it.overlaps(layer) }.toTypedArray()
+                layer.addCoveredBy(coveredBy)
 
                 if (layer.isOpaque) {
                     opaqueLayers.add(layer)
@@ -102,13 +111,39 @@
         }
     }
 
+    fun getLayerById(layerId: Int): Layer? = this.flattenedLayers.firstOrNull { it.id == layerId }
+
+    /**
+     * Checks the transform of any layer is not a simple rotation
+     */
+    fun isAnimating(windowName: String = ""): Boolean {
+        val layers = visibleLayers.filter { it.name.contains(windowName) }
+        return layers.any { layer -> !layer.transform.isSimpleRotation }
+    }
+
     /**
      * Check if at least one window which matches provided window name is visible.
      */
     fun isVisible(windowName: String): Boolean =
-        visibleLayers.any { it.name == windowName }
+        visibleLayers.any { it.name.contains(windowName) }
+
+    fun asTrace(): LayersTrace = LayersTrace(arrayOf(this), source = "")
 
     override fun toString(): String {
-        return prettyTimestamp(timestamp)
+        return "${prettyTimestamp(timestamp)} (timestamp=$timestamp)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is LayerTraceEntry && other.timestamp == this.timestamp
+    }
+
+    override fun hashCode(): Int {
+        var result = timestamp.hashCode()
+        result = 31 * result + hwcBlob.hashCode()
+        result = 31 * result + where.hashCode()
+        result = 31 * result + displays.contentHashCode()
+        result = 31 * result + isVisible.hashCode()
+        result = 31 * result + flattenedLayers.contentHashCode()
+        return result
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt
index 446678f..3bcd774 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt
@@ -21,9 +21,10 @@
  */
 class LayerTraceEntryBuilder(
     timestamp: Any,
-    layers: List<Layer>,
-    val hwcBlob: String = "",
-    val where: String = ""
+    layers: Array<Layer>,
+    private val displays: Array<Display>,
+    private val hwcBlob: String = "",
+    private val where: String = ""
 ) {
     // Necessary for compatibility with JS number type
     private val timestamp: Long = "$timestamp".toLong()
@@ -31,7 +32,7 @@
     private val orphans = mutableListOf<Layer>()
     private val layers = setLayers(layers)
 
-    private fun setLayers(layers: List<Layer>): Map<Int, Layer> {
+    private fun setLayers(layers: Array<Layer>): Map<Int, Layer> {
         val result = mutableMapOf<Int, Layer>()
         layers.forEach { layer ->
             val id = layer.id
@@ -129,6 +130,6 @@
         // Fail if we find orphan layers.
         notifyOrphansLayers()
 
-        return LayerTraceEntry(timestamp, hwcBlob, where, rootLayers.toTypedArray())
+        return LayerTraceEntry(timestamp, hwcBlob, where, displays, rootLayers.toTypedArray())
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt
index 5162b7c..988f357 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt
@@ -27,15 +27,46 @@
  * access internal Java/Android functionality
  *
  */
-open class LayersTrace(
-    override val entries: List<LayerTraceEntry>,
-    override val source: String = "",
-    override val sourceChecksum: String = ""
-) : ITrace<LayerTraceEntry>, List<LayerTraceEntry> by entries {
-    constructor(entry: LayerTraceEntry): this(listOf(entry))
+data class LayersTrace(
+    override val entries: Array<LayerTraceEntry>,
+    override val source: String = ""
+) : ITrace<LayerTraceEntry>, List<LayerTraceEntry> by entries.toList() {
+    constructor(entry: LayerTraceEntry): this(arrayOf(entry))
 
     override fun toString(): String {
         return "LayersTrace(Start: ${entries.first()}, " +
             "End: ${entries.last()})"
     }
-}
\ No newline at end of file
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is LayersTrace) return false
+
+        if (!entries.contentEquals(other.entries)) return false
+        if (source != other.source) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = entries.contentHashCode()
+        result = 31 * result + source.hashCode()
+        return result
+    }
+
+    /**
+     * Split the trace by the start and end timestamp.
+     *
+     * @param from the start timestamp
+     * @param to the end timestamp
+     * @return the subtrace trace(from, to)
+     */
+    fun filter(from: Long, to: Long): LayersTrace {
+        return LayersTrace(
+            this.entries
+                .dropWhile { it.timestamp < from }
+                .dropLastWhile { it.timestamp > to }
+                .toTypedArray(),
+            source = "")
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt
index 144fbab..2c05628 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt
@@ -16,7 +16,9 @@
 
 package com.android.server.wm.traces.common.layers
 
+import com.android.server.wm.traces.common.FloatFormatter
 import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.service.PlatformConsts
 
 open class Transform(val type: Int?, val matrix: Matrix) {
 
@@ -46,6 +48,20 @@
             return matrix.dsdx * matrix.dtdy != matrix.dtdx * matrix.dsdy
         }
 
+    fun getRotation(): Int {
+        if (type == null) {
+            return PlatformConsts.ROTATION_0
+        }
+
+        return when {
+            type.isFlagClear(SCALE_VAL or ROTATE_VAL or TRANSLATE_VAL) -> PlatformConsts.ROTATION_0
+            type.isFlagSet(ROT_90_VAL) -> PlatformConsts.ROTATION_90
+            type.isFlagSet(FLIP_V_VAL or FLIP_H_VAL) -> PlatformConsts.ROTATION_180
+            type.isFlagSet(ROT_90_VAL or FLIP_V_VAL or FLIP_H_VAL) -> PlatformConsts.ROTATION_270
+            else -> PlatformConsts.ROTATION_0
+        }
+    }
+
     private val typeFlags: Array<String>
         get() {
             if (type == null) {
@@ -100,6 +116,8 @@
         return "$transformType ${matrix.prettyPrint()}"
     }
 
+    override fun toString(): String = prettyPrint()
+
     fun apply(bounds: RectF?): RectF {
         return multiplyRect(matrix, bounds ?: RectF.EMPTY)
     }
@@ -116,7 +134,17 @@
         val dtdy: Float,
         val ty: Float
     ) {
-        fun prettyPrint(): String = "dsdx:$dsdx   dtdx:$dtdx   dsdy:$dsdy   dtdy:$dtdy"
+        fun prettyPrint(): String {
+            val dsdx = FloatFormatter.format(dsdx)
+            val dtdx = FloatFormatter.format(dtdx)
+            val dsdy = FloatFormatter.format(dsdy)
+            val dtdy = FloatFormatter.format(dtdy)
+            return "dsdx:$dsdx   dtdx:$dtdx   dsdy:$dsdy   dtdy:$dtdy"
+        }
+
+        companion object {
+            val EMPTY: Matrix = Matrix(0f, 0f, 0f, 0f, 0f, 0f)
+        }
     }
 
     private data class Vec2(val x: Float, val y: Float)
@@ -150,6 +178,8 @@
     }
 
     companion object {
+        val EMPTY: Transform = Transform(type = null, matrix = Matrix.EMPTY)
+
         /* transform type flags */
         const val TRANSLATE_VAL = 0x0001
         const val ROTATE_VAL = 0x0002
@@ -173,4 +203,22 @@
             return this and bits == bits
         }
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Transform) return false
+
+        if (type != other.type) return false
+        if (matrix != other.matrix) return false
+        if (isSimpleRotation != other.isSimpleRotation) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = type ?: 0
+        result = 31 * result + matrix.hashCode()
+        result = 31 * result + isSimpleRotation.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt
new file mode 100644
index 0000000..cb04966
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Interface for the WM Flicker Tag Processor component.
+ */
+interface ITagProcessor {
+    /**
+     * Adds tags to the received traces.
+     *
+     * @param wmTrace Window Manager trace
+     * @param layersTrace Surface Flinger trace
+     */
+    fun generateTags(wmTrace: WindowManagerTrace, layersTrace: LayersTrace): TagTrace
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt
new file mode 100644
index 0000000..a3dcea8
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service
+
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Interface for the WM Flicker Service Assertor component.
+ */
+interface ITransitionAssertor {
+    /**
+     * Analyzes a [WindowManagerTrace] and/or a [LayersTrace] trace to detect flickers.
+     *
+     * @param tag Tag for the transition
+     * @param wmTrace Window Manager trace
+     * @param layersTrace Surface Flinger trace
+     * @return An error trace
+     */
+    fun analyze(tag: Tag, wmTrace: WindowManagerTrace, layersTrace: LayersTrace): ErrorTrace
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt
new file mode 100644
index 0000000..0a13329
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service
+
+object PlatformConsts {
+    /**
+     * The default Display id, which is the id of the primary display assuming there is one.
+     *
+     * Duplicated from [Display.DEFAULT_DISPLAY] because this class is used by JVM and KotlinJS
+     */
+    const val DEFAULT_DISPLAY = 0
+
+    /**
+     * Window type: an application window that serves as the "base" window
+     * of the overall application
+     *
+     * Duplicated from [WindowManager.LayoutParams.TYPE_BASE_APPLICATION] because this class
+     * is used by JVM and KotlinJS
+     */
+    const val TYPE_BASE_APPLICATION = 1
+
+    /**
+     * Window type: special application window that is displayed while the
+     * application is starting
+     *
+     * Duplicated from [WindowManager.LayoutParams.TYPE_APPLICATION_STARTING] because this class
+     * is used by JVM and KotlinJS
+     */
+    const val TYPE_APPLICATION_STARTING = 3
+
+    /**
+     * Rotation constant: 0 degree rotation (natural orientation)
+     *
+     * Duplicated from [Surface.ROTATION_0] because this class is used by JVM and KotlinJS
+     */
+    const val ROTATION_0 = 0
+
+    /**
+     * Rotation constant: 90 degree rotation.
+     *
+     * Duplicated from [Surface.ROTATION_90] because this class is used by JVM and KotlinJS
+     */
+    const val ROTATION_90 = 1
+
+    /**
+     * Rotation constant: 180 degree rotation.
+     *
+     * Duplicated from [Surface.ROTATION_180] because this class is used by JVM and KotlinJS
+     */
+    const val ROTATION_180 = 2
+
+    /**
+     * Rotation constant: 270 degree rotation.
+     *
+     * Duplicated from [Surface.ROTATION_270] because this class is used by JVM and KotlinJS
+     */
+    const val ROTATION_270 = 3
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt
new file mode 100644
index 0000000..ef46d8e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.service.processors.AppCloseProcessor
+import com.android.server.wm.traces.common.service.processors.AppLaunchProcessor
+import com.android.server.wm.traces.common.service.processors.ImeAppearProcessor
+import com.android.server.wm.traces.common.service.processors.ImeDisappearProcessor
+import com.android.server.wm.traces.common.service.processors.PipEnterProcessor
+import com.android.server.wm.traces.common.service.processors.PipExitProcessor
+import com.android.server.wm.traces.common.service.processors.PipExpandProcessor
+import com.android.server.wm.traces.common.service.processors.PipResizeProcessor
+import com.android.server.wm.traces.common.service.processors.RotationProcessor
+import com.android.server.wm.traces.common.tags.TagState
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * Invokes all concrete tag producers and writes to a .winscope file
+ *
+ * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility
+ *
+ * The traces are passed as constructor arguments due to name mangling in KotlinJS
+ *
+ * @param logger Platform dependent function for logging
+ * @param wmTrace WindowManager trace
+ * @param layersTrace SurfaceFlinger trace
+ */
+class TaggingEngine(
+    private val wmTrace: WindowManagerTrace,
+    private val layersTrace: LayersTrace,
+    private val logger: (String) -> Unit
+) {
+    private val transitions = listOf(
+        // TODO: Keep adding new transition processors to invoke
+        RotationProcessor(logger),
+        AppLaunchProcessor(logger),
+        AppCloseProcessor(logger),
+        ImeAppearProcessor(logger),
+        ImeDisappearProcessor(logger),
+        PipEnterProcessor(logger),
+        PipResizeProcessor(logger),
+        PipExpandProcessor(logger),
+        PipExitProcessor(logger)
+    )
+
+    /**
+     * Generate tags denoting start and end points for all [transitions] within traces
+     */
+    fun run(): TagTrace {
+        val allStates = transitions.flatMap {
+            logger.invoke("Generating tags for ${it::class.simpleName}")
+            it.generateTags(wmTrace, layersTrace).entries.asList()
+        }
+
+        /**
+         * Ensure all tag states with the same timestamp are merged
+         */
+        val tagStates = allStates.distinct()
+            .groupBy({ it.timestamp }, { it.tags.asList() })
+            .mapValues { (key, value) -> TagState(key.toString(), value.flatten().toTypedArray()) }
+            .values.toTypedArray()
+
+        return TagTrace(tagStates, source = "")
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt
new file mode 100644
index 0000000..7b3ffff
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformIdentity
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.service.PlatformConsts.TYPE_APPLICATION_STARTING
+import com.android.server.wm.traces.common.service.PlatformConsts.TYPE_BASE_APPLICATION
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+
+/**
+ * This processor creates tags when an app is closed.
+ * @param logger logs by invoking any event messages
+ */
+class AppCloseProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.APP_CLOSE
+    private val areLayersAnimating = WindowManagerConditionsFactory.hasLayersAnimating()
+    private val wmStateIdle = WindowManagerConditionsFactory
+        .isAppTransitionIdle(/* default display */ 0)
+    private val wmStateComplete = WindowManagerConditionsFactory.isWMStateComplete()
+    private val translatingWindows =
+        HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        RetrieveClosingAppLayerId(tags)
+
+    /**
+     * Initial FSM state that passes the current app launch activity if any to the next state.
+     * Closing app is also not transforming and has transform identity
+     */
+    inner class RetrieveClosingAppLayerId(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val isStableState = wmStateIdle.isSatisfied(current) ||
+                wmStateComplete.isSatisfied(current) ||
+                areLayersAnimating.negate().isSatisfied(current)
+            val currentAppWindows = current.wmState.rootTasks.flatMap { task ->
+                // No app launch activities and only resuming activities
+                val activities = task.activities.filter { activity ->
+                    activity.state == "RESUMED" && activity.isVisible &&
+                    activity.children.filterIsInstance<WindowState>().none { window ->
+                        window.attributes.type == TYPE_APPLICATION_STARTING
+                    }
+                }
+                activities.flatMap { activity ->
+                    activity.children.filterIsInstance<WindowState>().filter { window ->
+                        window.isVisible && window.attributes.type == TYPE_BASE_APPLICATION
+                    }
+                }
+            }
+
+            // Only one closing app. This processor ignores app pairs situations.
+            if (currentAppWindows.size == 1 && isStableState) {
+                val isNotTransforming = isLayerTransformIdentity(currentAppWindows.first().layerId)
+                    .isSatisfied(current)
+                if (isNotTransforming) {
+                    return WaitLayerAnimationComplete(tags, currentAppWindows.first())
+                }
+            }
+            return this
+        }
+    }
+
+    /**
+     * FSM State that waits until the closing app has finished and stopped transforming.
+     */
+    inner class WaitLayerAnimationComplete(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val appWindow: WindowState
+    ) : BaseState(tags) {
+        private val layerId = appWindow.layerId
+        private val isTranslating = isLayerTransformFlagSet(layerId, Transform.TRANSLATE_VAL)
+
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+            val startedTransforming = isTranslating.isSatisfied(current) &&
+                isTranslating.negate().isSatisfied(previous)
+            val isStableState = wmStateIdle.isSatisfied(current) ||
+                wmStateComplete.isSatisfied(current) ||
+                areLayersAnimating.negate().isSatisfied(current)
+
+            val finishedClosing = isLayerTransformIdentity(layerId).isSatisfied(current) &&
+                current.layerState.getLayerById(layerId)?.isHiddenByParent ?: false
+
+            if (startedTransforming) {
+                translatingWindows[appWindow.token] = current
+            } else if (finishedClosing && isStableState) {
+                val deviceStateDump = translatingWindows[appWindow.token]
+                if (deviceStateDump != null) {
+                    addStartTransitionTag(deviceStateDump, transition,
+                        layerId = layerId,
+                        windowToken = appWindow.token
+                    )
+                    addEndTransitionTag(current, transition,
+                        layerId = layerId,
+                        windowToken = appWindow.token
+                    )
+                    return RetrieveClosingAppLayerId(tags)
+                }
+            }
+            return this
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt
new file mode 100644
index 0000000..cb75954
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when an app is launched.
+ * @param logger logs by invoking any event messages
+ */
+class AppLaunchProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.APP_LAUNCH
+    private val windowsBecomeVisible =
+        HashMap<Int, DeviceStateDump<WindowManagerState, LayerTraceEntry>>()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitUntilWindowIsInVisibleActivity(tags)
+
+    /**
+     * FSM state that stores any newly visible window activities (start tag)
+     * and when their layers stop scaling (end tag).
+     */
+    inner class WaitUntilWindowIsInVisibleActivity(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+            val prevVisibleWindows = previous.wmState.visibleWindows
+            val newlyVisibleWindows = current.wmState.visibleWindows.filterNot { window ->
+                prevVisibleWindows.any { it.token == window.token }
+            }
+
+            // Wait until layer is no longer scaling
+            val appLaunchedLayers = windowsBecomeVisible.filterKeys { layerId ->
+                val currDumpLayer = current.layerState.getLayerById(layerId)
+                (previous.layerState.getLayerById(layerId)?.isScaling == true &&
+                currDumpLayer?.isScaling == false)
+            }
+
+            // Only want to tag when one app is being launched.
+            // Other scenarios like app pairs enter are ignored.
+            if (newlyVisibleWindows.size == 1) {
+                windowsBecomeVisible[newlyVisibleWindows.first().layerId] = previous
+            } else if (appLaunchedLayers.isNotEmpty()) {
+                val firstDump = appLaunchedLayers.entries.first()
+                val layerId = firstDump.key
+                addStartTransitionTag(firstDump.value, transition,
+                    layerId = layerId,
+                    timestamp = firstDump.value.layerState.timestamp
+                )
+                addEndTransitionTag(current, transition,
+                    layerId = layerId,
+                    timestamp = current.layerState.timestamp
+                )
+                windowsBecomeVisible.clear()
+            }
+            return this
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt
new file mode 100644
index 0000000..2b7bb3b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * Base state for the FSM, check if there are more WM and SF states to process
+ * and ensure there is always a 1:1 correspondence between start and end tags.
+ * If the location of the end of the transition wasn't found, add an end tag at end of trace.
+ */
+abstract class BaseFsmState(
+    tags: MutableMap<Long, MutableList<Tag>>,
+    internal val logger: (String) -> Unit,
+    internal val transition: Transition
+) : FSMState(tags) {
+    protected abstract fun doProcessState(
+        previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+        current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+        next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+    ): FSMState
+
+    override fun process(
+        previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+        current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+        next: DeviceStateDump<WindowManagerState, LayerTraceEntry>?
+    ): FSMState? {
+        return if (next == null) {
+            // last state
+            val timestamp = current.layerState.timestamp
+            logger.invoke("($timestamp) Trace has reached the end")
+            if (hasOpenTag()) {
+                logger.invoke("($timestamp) Has an open tag, closing it on the last SF state")
+                addEndTransitionTag(current, transition)
+            }
+            null
+        } else {
+            doProcessState(previous, current, next)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt
new file mode 100644
index 0000000..6c661ae
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Represents the Finite State Machine used by tagging processors and implements adding start and
+ * end tags.
+ * @param tags represents the map of timestamps associated with tag(s).
+ */
+abstract class FSMState(protected val tags: MutableMap<Long, MutableList<Tag>>) {
+    abstract fun process(
+        previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+        current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+        next: DeviceStateDump<WindowManagerState, LayerTraceEntry>?
+    ): FSMState?
+
+    protected fun addStartTransitionTag(
+        state: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+        transition: Transition,
+        layerId: Int = 0,
+        windowToken: String = "",
+        taskId: Int = 0,
+        timestamp: Long = min(state.wmState.timestamp, state.layerState.timestamp)
+    ) {
+        val tagId = ++lastTagId
+        val startTag = Tag(id = tagId, transition, isStartTag = true, layerId = layerId,
+            windowToken = windowToken, taskId = taskId)
+        if (!tags.containsKey(timestamp)) {
+            tags[timestamp] = mutableListOf()
+        }
+        tags.getValue(timestamp).add(startTag)
+    }
+
+    protected fun addEndTransitionTag(
+        state: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+        transition: Transition,
+        layerId: Int = 0,
+        windowToken: String = "",
+        taskId: Int = 0,
+        timestamp: Long = max(state.wmState.timestamp, state.layerState.timestamp)
+    ) {
+        val endTag = Tag(id = lastTagId, transition, isStartTag = false, layerId = layerId,
+            windowToken = windowToken, taskId = taskId)
+        if (!tags.containsKey(timestamp)) {
+            tags[timestamp] = mutableListOf()
+        }
+        tags.getValue(timestamp).add(endTag)
+    }
+
+    protected fun hasOpenTag() = tags.values.flatten().size % 2 != 0
+
+    companion object {
+        private var lastTagId = -1
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt
new file mode 100644
index 0000000..44b5c3c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.ConditionList
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isImeShown
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerVisible
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when the keyboard starts and finishes appearing.
+ * @param logger logs by invoking any event messages
+ */
+class ImeAppearProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.IME_APPEAR
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitInputMethodVisible(tags)
+
+    /**
+     * FSM state that waits until the InputMethod is visible in both WM and SF.
+     */
+    inner class WaitInputMethodVisible(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        private val newImeVisible = isImeShown(PlatformConsts.DEFAULT_DISPLAY)
+        private val prevImeInvisible = newImeVisible.negate()
+
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+
+            return if (newImeVisible.isSatisfied(current) &&
+                prevImeInvisible.isSatisfied(previous)) {
+                processInputMethodVisible(current)
+            } else {
+                this
+            }
+        }
+
+        private fun processInputMethodVisible(
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            logger.invoke("(${current.layerState.timestamp}) IME appear started.")
+            // add factory method as well
+            val inputMethodLayer = current.layerState.visibleLayers.first {
+                it.name.contains(FlickerComponentName.IME.toLayerName())
+            }
+            addStartTransitionTag(current, transition, layerId = inputMethodLayer.id)
+            return WaitImeAppearFinished(tags, inputMethodLayer.id)
+        }
+    }
+
+    /**
+     * FSM state to check when the Ime Appear has finished by opaque color alpha of input method
+     * and it has finished transforming and scaling.
+     */
+    inner class WaitImeAppearFinished(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val layerId: Int
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val isImeAppearFinished = isImeAppearFinished.isSatisfied(current)
+
+            return if (isImeAppearFinished) {
+                // tag on the last complete state at the start
+                logger.invoke("(${current.layerState.timestamp}) Ime appear end detected.")
+                addEndTransitionTag(current, transition, layerId = layerId)
+                // return to start to wait for a second IME appear
+                WaitInputMethodVisible(tags)
+            } else {
+                logger.invoke("(${current.layerState.timestamp}) Ime appear hasn't finished.")
+                this
+            }
+        }
+
+        private val isImeAppearFinished = ConditionList(listOf(
+            isLayerVisible(FlickerComponentName.IME),
+            isLayerColorAlphaOne(FlickerComponentName.IME),
+            isLayerTransformFlagSet(FlickerComponentName.IME, Transform.TRANSLATE_VAL),
+            isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL).negate()
+        ))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt
new file mode 100644
index 0000000..3aff56f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.ConditionList
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isImeShown
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when the keyboard starts and finishes disappearing.
+ * @param logger logs by invoking any event messages
+ */
+class ImeDisappearProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.IME_DISAPPEAR
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitImeDisappearStart(tags)
+
+    /**
+     * FSM state that waits until the IME begins to disappear
+     * Different conditions required for IME closing by gesture (layer color alpha < 1), compared
+     * to IME closing via app close (layer translate SCALE_VAL bit set)
+     */
+    inner class WaitImeDisappearStart(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        private val isImeShown = isImeShown(PlatformConsts.DEFAULT_DISPLAY)
+        private val isImeAppeared =
+            ConditionList(listOf(
+                isImeShown,
+                isLayerColorAlphaOne(FlickerComponentName.IME),
+                isLayerTransformFlagSet(FlickerComponentName.IME, Transform.TRANSLATE_VAL),
+                isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL).negate()
+            ))
+        private val isImeDisappearByGesture =
+            ConditionList(listOf(
+                isImeShown,
+                isLayerColorAlphaOne(FlickerComponentName.IME).negate()
+            ))
+        private val isImeDisappearByAppClose =
+            ConditionList(listOf(
+                isImeShown,
+                isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL)
+            ))
+
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+
+            return if (isImeAppeared.isSatisfied(previous) &&
+                (isImeDisappearByGesture.isSatisfied(current) ||
+                isImeDisappearByAppClose.isSatisfied(current))) {
+                processImeDisappearing(current)
+            } else {
+                logger.invoke("(${current.layerState.timestamp}) IME disappear not started.")
+                this
+            }
+        }
+
+        private fun processImeDisappearing(
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            logger.invoke("(${current.layerState.timestamp}) IME disappear started.")
+            val inputMethodLayer = current.layerState.visibleLayers.first {
+                it.name.contains(FlickerComponentName.IME.toLayerName())
+            }
+            addStartTransitionTag(current, transition, layerId = inputMethodLayer.id)
+            return WaitImeDisappearFinished(tags, inputMethodLayer.id)
+        }
+    }
+
+    /**
+     * FSM state to check when the IME disappear has finished i.e. when the input method layer is
+     * no longer visible.
+     */
+    inner class WaitImeDisappearFinished(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val layerId: Int
+    ) : BaseState(tags) {
+        private val imeNotShown = isImeShown(PlatformConsts.DEFAULT_DISPLAY).negate()
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            return if (imeNotShown.isSatisfied(current)) {
+                // tag on the last complete state at the start
+                logger.invoke("(${current.layerState.timestamp}) IME disappear end detected.")
+                addEndTransitionTag(current, transition, layerId = layerId)
+                // return to start to wait for a second IME disappear
+                WaitImeDisappearStart(tags)
+            } else {
+                logger.invoke("(${current.layerState.timestamp}) IME disappear not finished.")
+                this
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt
new file mode 100644
index 0000000..4ec4099
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.ConditionList
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasPipWindow
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isPipWindowLayerSizeMatch
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * Processor to detect PIP mode enter.
+ * Waits for a window to enter pip mode, then tags transition start at the last moment before
+ * corresponding layer started scaling.
+ * Tags transition end when window and layer stop animating and their sizes match.
+ */
+class PipEnterProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.PIP_ENTER
+    private val allScalingLayers = mutableMapOf<Int, Long>()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitPipEnterStart(tags)
+
+    /**
+     * FSM state that waits for a pinned window to appear. Until this occurs, it watches all
+     * scaling layers, recording when they started to scale in the companion object. When the
+     * pinned window has appeared, it adds a start tag at the timestamp at which the layer with
+     * the same id as the pinned window layerId started to scale.
+     */
+    inner class WaitPipEnterStart(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+
+        private fun getScalingLayers(
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): List<Layer> = current.layerState.flattenedLayers.filter {
+            WindowManagerConditionsFactory.isLayerTransformFlagSet(
+                it.id,
+                Transform.SCALE_VAL
+            ).isSatisfied(current)
+        }
+
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+
+            // get current scaling layers and add to allScalingLayers map for whole trace
+            val scalingLayers = getScalingLayers(current)
+            val scalingLayerIds = scalingLayers.map { it.id }
+            scalingLayerIds.forEach {
+                allScalingLayers.getOrPut(it) { previous.layerState.timestamp }
+            }
+
+            // remove all layers that are no longer scaling from allScalingLayers map
+            val notScalingLayerIds = allScalingLayers.keys.filter { !scalingLayerIds.contains(it) }
+            notScalingLayerIds.forEach {
+                allScalingLayers.remove(it)
+            }
+
+            return if (hasPipWindow().isSatisfied(current) &&
+                hasPipWindow().negate().isSatisfied(previous)) {
+                processPipEnterStart(current)
+            } else {
+                logger.invoke("(${current.layerState.timestamp}) PIP enter not started.")
+                this
+            }
+        }
+
+        private fun processPipEnterStart(
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val pipWindow = current.wmState.pinnedWindows.first()
+            val startTimestamp = allScalingLayers[pipWindow.layerId]
+
+            if (startTimestamp != null) {
+                addStartTransitionTag(
+                    current,
+                    transition,
+                    layerId = pipWindow.layerId,
+                    timestamp = startTimestamp
+                )
+            } else {
+                addStartTransitionTag(
+                    current,
+                    transition,
+                    layerId = pipWindow.layerId
+                )
+            }
+            // reset all scaling layers
+            allScalingLayers.clear()
+
+            logger.invoke("($startTimestamp) PIP enter started.")
+            return WaitPipEnterFinished(tags, pipWindow.layerId)
+        }
+    }
+
+    /**
+     * FSM state to check when the PIP enter has finished. This is when the pinned window
+     * has the same size as the associated layer.
+     */
+    inner class WaitPipEnterFinished(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val layerId: Int
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            return if (isPipEnterFinished.isSatisfied(current)) {
+                // tag on the last complete state at the start
+                logger.invoke("(${current.wmState.timestamp}) PIP enter finished.")
+                addEndTransitionTag(current, transition, layerId = layerId)
+
+                // return to start to wait for a second PIP enter
+                WaitPipEnterStart(tags)
+            } else {
+                logger.invoke("(${current.wmState.timestamp}) PIP enter not finished.")
+                this
+            }
+        }
+
+        private val isPipEnterFinished = ConditionList(listOf(
+            isAppTransitionIdle(PlatformConsts.DEFAULT_DISPLAY),
+            hasPipWindow(),
+            isPipWindowLayerSizeMatch(layerId)
+        ))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt
new file mode 100644
index 0000000..3ed713e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasLayersAnimating
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerVisible
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isWMStateComplete
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when the pip window is exited directly to home screen or a
+ * different app altogether.
+ * @param logger logs by invoking any event messages
+ */
+class PipExitProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.PIP_EXIT
+    private val scalingWindows =
+        HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>()
+    private val areLayersAnimating = hasLayersAnimating()
+    private val wmStateIdle = isAppTransitionIdle(/* default display */ 0)
+    private val wmStateComplete = isWMStateComplete()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+            WaitPinnedWindowSwipedOrFading(tags)
+
+    /**
+     * Initial FSM state which waits until the app window in pip mode starts to change opacity.
+     */
+    inner class WaitPinnedWindowSwipedOrFading(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+            // There can only ever be one pinned window at a time
+            val nextPinnedWindow = next.wmState.pinnedWindows.firstOrNull()
+            val currPinnedWindow = current.wmState.pinnedWindows.firstOrNull() ?: return this
+
+            if (nextPinnedWindow == null) {
+                val dump = scalingWindows[currPinnedWindow.token]
+                if (dump != null) {
+                    // Pip app unpinned so let's tag.
+                    addStartTransitionTag(dump, transition,
+                        layerId = currPinnedWindow.layerId,
+                        windowToken = currPinnedWindow.token
+                    )
+                    addEndTransitionTag(previous, transition,
+                        layerId = currPinnedWindow.layerId,
+                        windowToken = currPinnedWindow.token
+                    )
+                }
+                return this
+            }
+
+            // close pip by swiping
+            val isScaling = isLayerTransformFlagSet(currPinnedWindow.layerId, Transform.SCALE_VAL)
+            val movingPinnedWindow =
+                isScaling.isSatisfied(current) && isScaling.negate().isSatisfied(previous)
+
+            // close pip by pressing dismiss button
+            val colorAlphaIsOne = isLayerColorAlphaOne(currPinnedWindow.layerId)
+            val pinnedWindowFading = colorAlphaIsOne.negate().isSatisfied(current) &&
+                colorAlphaIsOne.isSatisfied(previous) &&
+                isScaling.negate().isSatisfied(current)
+
+            return when {
+                movingPinnedWindow -> {
+                    // Record last time when pip app started scaling
+                    scalingWindows[currPinnedWindow.token] = current
+                    this
+                }
+                pinnedWindowFading -> {
+                    addStartTransitionTag(previous, transition,
+                        layerId = currPinnedWindow.layerId,
+                        windowToken = currPinnedWindow.token
+                    )
+                    WaitUntilPipColorAlphaIsOneAndInvisible(tags, currPinnedWindow.layerId)
+                }
+                else -> {
+                    this
+                }
+            }
+        }
+    }
+
+    inner class WaitUntilPipColorAlphaIsOneAndInvisible(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val layerId: Int
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val layerInvisible = isLayerVisible(layerId).negate().isSatisfied(current)
+            val layerColorAlphaOne = isLayerColorAlphaOne(layerId).isSatisfied(current)
+            val isStableState = wmStateIdle.isSatisfied(current) ||
+                wmStateComplete.isSatisfied(current) ||
+                areLayersAnimating.negate().isSatisfied(current)
+
+            return if (layerInvisible && layerColorAlphaOne && isStableState) {
+                addEndTransitionTag(current, transition, layerId = layerId)
+                WaitPinnedWindowSwipedOrFading(tags)
+            } else {
+                this
+            }
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt
new file mode 100644
index 0000000..c15b731
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformIdentity
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when an pip window starts to expand from window to full screen app.
+ * @param logger logs by invoking any event messages
+ */
+class PipExpandProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.PIP_EXPAND
+    private val scalingLayers =
+        HashMap<Int, DeviceStateDump<WindowManagerState, LayerTraceEntry>>()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitUntilAppIsNoLongerPinned(tags)
+
+    /**
+     * We wait until the pip app is no longer pinned and is ready to expand.
+     */
+    inner class WaitUntilAppIsNoLongerPinned(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            // Pinned window is no longer pinned
+            val prevPinnedWindows = previous?.wmState?.pinnedWindows?.toList() ?: emptyList()
+            val currPinnedWindows = current.wmState.pinnedWindows.toList()
+
+            if (prevPinnedWindows.isNotEmpty() && currPinnedWindows.isEmpty()) {
+                return WaitUntilAppCompletesExpanding(tags, prevPinnedWindows.first().layerId)
+            }
+            return this
+        }
+    }
+
+    /**
+     * FSMState when app has been unpinned and we track its corresponding layer.
+     * We record every time the layer is scaling and check it has transform identity
+     * with increased bounds.
+     */
+    inner class WaitUntilAppCompletesExpanding(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val layerId: Int
+    ) : BaseState(tags) {
+        private val isScaling = isLayerTransformFlagSet(layerId, Transform.SCALE_VAL)
+        private val isIdentity = isLayerTransformIdentity(layerId)
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            if (previous == null) return this
+            if (previous.wmState.pinnedWindows.isNotEmpty()) {
+                return WaitUntilAppIsNoLongerPinned(tags)
+            }
+
+            val startedScaling = isScaling.isSatisfied(current) &&
+                isScaling.negate().isSatisfied(previous)
+
+            val currLayerBounds = current.layerState.getLayerById(layerId)?.bounds ?: return this
+            val prevLayerBounds = previous.layerState.getLayerById(layerId)?.bounds ?: return this
+            val finishedExpanding = isScaling.isSatisfied(previous) &&
+                isIdentity.isSatisfied(current) &&
+                currLayerBounds.height > prevLayerBounds.height &&
+                currLayerBounds.width > prevLayerBounds.width
+
+            if (startedScaling) {
+                scalingLayers[layerId] = current
+            } else if (finishedExpanding) {
+                val dump = scalingLayers[layerId]
+                if (dump != null) {
+                    addStartTransitionTag(current, transition,
+                        layerId = layerId,
+                        timestamp = dump.layerState.timestamp
+                    )
+                    addEndTransitionTag(current, transition,
+                        layerId = layerId,
+                        timestamp = current.layerState.timestamp
+                    )
+                    scalingLayers.clear()
+                    return WaitUntilAppIsNoLongerPinned(tags)
+                }
+            }
+            return this
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt
new file mode 100644
index 0000000..7bb89d2
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * This processor creates tags when the pip window is resized but is still in pip mode.
+ * @param logger logs by invoking any event messages
+ */
+class PipResizeProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.PIP_RESIZE
+    private val scalingWindows =
+        HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>()
+
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+        WaitUntilAppStopsAnimatingYetStillPinned(tags)
+
+    inner class WaitUntilAppStopsAnimatingYetStillPinned(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val currPinnedWindow = current.wmState.pinnedWindows.firstOrNull() ?: return this
+            previous?.wmState?.pinnedWindows?.firstOrNull() ?: return this
+
+            val isScaling = isLayerTransformFlagSet(currPinnedWindow.layerId, Transform.SCALE_VAL)
+            val startedScaling = isScaling.negate().isSatisfied(previous) &&
+                isScaling.isSatisfied(current)
+            if (startedScaling) {
+                // remember when pinned window/layer starting scaling
+                scalingWindows[currPinnedWindow.token] = previous
+            }
+
+            // Bounds have changed and layer no longer scaling
+            val currBounds = current.layerState.getLayerById(currPinnedWindow.layerId)?.bounds
+            val prevBounds = previous.layerState.getLayerById(currPinnedWindow.layerId)?.bounds
+            val finishedResizing = isScaling.isSatisfied(previous) &&
+                isScaling.negate().isSatisfied(current) &&
+                (currBounds?.height != prevBounds?.height) &&
+                (currBounds?.width != prevBounds?.width)
+
+            if (finishedResizing) {
+                val lastScaledDump = scalingWindows[currPinnedWindow.token]
+                if (lastScaledDump != null) {
+                    addStartTransitionTag(lastScaledDump, transition,
+                        layerId = currPinnedWindow.layerId,
+                        windowToken = currPinnedWindow.token,
+                        timestamp = lastScaledDump.layerState.timestamp
+                    )
+                    addEndTransitionTag(lastScaledDump, transition,
+                        layerId = currPinnedWindow.layerId,
+                        windowToken = currPinnedWindow.token,
+                        timestamp = current.layerState.timestamp
+                    )
+                    scalingWindows.remove(currPinnedWindow.token)
+                }
+            }
+            return this
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt
new file mode 100644
index 0000000..be29ced
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+
+/**
+ * Processor to detect rotations.
+ *
+ * First check the WM state for a rotation change, then wait the SF rotation
+ * to occur and both nav and status bars to appear
+ */
+class RotationProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) {
+    override val transition = Transition.ROTATION
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = InitialState(tags)
+
+    /**
+     * Initial FSM state, obtains the current display size and start searching
+     * for display size changes
+     */
+    inner class InitialState(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val currDisplayRect = current.wmState.displaySize()
+            logger.invoke("(${current.wmState.timestamp}) Initial state. " +
+                "Display size $currDisplayRect")
+            return WaitDisplayRectChange(tags, currDisplayRect)
+        }
+    }
+
+    /**
+     * FSM state when the display size has not changed since [InitialState]
+     */
+    inner class WaitDisplayRectChange(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val currDisplayRect: RectF
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val newWmDisplayRect = current.wmState.displaySize()
+            val newLayersDisplayRect = current.layerState.screenBounds()
+
+            return when {
+                // WM display changed first (Regular rotation)
+                // SF display changed first (Seamless rotation)
+                newWmDisplayRect != currDisplayRect || newLayersDisplayRect != currDisplayRect -> {
+                    requireNotNull(previous) { "Should have a previous state" }
+                    val rect = if (newWmDisplayRect != currDisplayRect) {
+                        newWmDisplayRect
+                    } else {
+                        newLayersDisplayRect
+                    }
+                    processDisplaySizeChange(previous, rect)
+                }
+                else -> {
+                    logger.invoke("(${current.wmState.timestamp}) No display size change")
+                    this
+                }
+            }
+        }
+
+        private fun processDisplaySizeChange(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            newDisplayRect: RectF
+        ): FSMState {
+            logger.invoke("(${previous.wmState.timestamp}) Display size changed " +
+                "to $newDisplayRect")
+            // tag on the last complete state at the start
+            logger.invoke("(${previous.wmState.timestamp}) Tagging transition start")
+            addStartTransitionTag(previous, transition)
+            return WaitRotationFinished(tags)
+        }
+    }
+
+    /**
+     * FSM state for when the animation occurs in the SF trace
+     */
+    inner class WaitRotationFinished(tags: MutableMap<Long, MutableList<Tag>>) : BaseState(tags) {
+        private val rotationLayerExists = WindowManagerConditionsFactory
+            .isLayerVisible(FlickerComponentName.ROTATION)
+        private val backSurfaceLayerExists = WindowManagerConditionsFactory
+            .isLayerVisible(FlickerComponentName.BACK_SURFACE)
+        private val areLayersAnimating = WindowManagerConditionsFactory.hasLayersAnimating()
+        private val wmStateIdle = WindowManagerConditionsFactory
+            .isAppTransitionIdle(/* default display */ 0)
+        private val wmStateComplete = WindowManagerConditionsFactory.isWMStateComplete()
+
+        override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState {
+            val anyLayerAnimating = areLayersAnimating.isSatisfied(current)
+            val rotationLayerExists = rotationLayerExists.isSatisfied(current)
+            val blackSurfaceLayerExists = backSurfaceLayerExists.isSatisfied(current)
+            val wmStateIdle = wmStateIdle.isSatisfied(current)
+            val wmStateComplete = wmStateComplete.isSatisfied(current)
+
+            val newWmDisplayRect = current.wmState.displaySize()
+            val newLayersDisplayRect = current.layerState.screenBounds()
+            val displaySizeDifferent = newWmDisplayRect != newLayersDisplayRect
+
+            val inRotation = anyLayerAnimating || rotationLayerExists || blackSurfaceLayerExists ||
+                displaySizeDifferent || !wmStateIdle || !wmStateComplete
+            logger.invoke("(${current.layerState.timestamp}) " +
+                "In rotation? $inRotation (" +
+                "anyLayerAnimating=$anyLayerAnimating, " +
+                "blackSurfaceLayerExists=$blackSurfaceLayerExists, " +
+                "rotationLayerExists=$rotationLayerExists, " +
+                "wmStateIdle=$wmStateIdle, " +
+                "wmStateComplete=$wmStateComplete, " +
+                "displaySizeDifferent=$displaySizeDifferent)")
+            return if (inRotation) {
+                this
+            } else {
+                // tag on the last complete state at the start
+                logger.invoke("(${current.layerState.timestamp}) Tagging transition end")
+                addEndTransitionTag(current, transition)
+                // return to start to wait for a second rotation
+                val lastDisplayRect = current.wmState.displaySize()
+                WaitDisplayRectChange(tags, lastDisplayRect)
+            }
+        }
+    }
+
+    companion object {
+        private fun LayerTraceEntry.screenBounds() = this.displays.minByOrNull { it.id }
+            ?.layerStackSpace?.toRectF() ?: this.children
+            .sortedBy { it.id }
+            .firstOrNull { it.isRootLayer }
+            ?.screenBounds ?: error("Unable to identify screen bounds (display is empty in proto)")
+
+        private fun WindowManagerState.displaySize() = getDefaultDisplay()
+            ?.displayRect?.toRectF() ?: RectF.EMPTY
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt
new file mode 100644
index 0000000..31b30ba
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.service.processors
+
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.service.ITagProcessor
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.TagState
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+
+/**
+ * This class implements the relevant methods such as generating tags, creating dumps for the
+ * WindowManager and SurfaceFlinger traces, and ensuring the 1:1 correspondence between the start
+ * and end tags invariant is maintained by [BaseFsmState].
+ */
+abstract class TransitionProcessor(internal val logger: (String) -> Unit) : ITagProcessor {
+    abstract val transition: Transition
+    abstract fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>): BaseState
+
+    abstract inner class BaseState(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseFsmState(tags, logger, transition) {
+        abstract override fun doProcessState(
+            previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?,
+            current: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
+            next: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        ): FSMState
+    }
+
+    /**
+     * Add the start and end tags corresponding to the transition from
+     * the WindowManager and SurfaceFlinger traces
+     * @param wmTrace - WindowManager trace
+     * @param layersTrace - SurfaceFlinger trace
+     * @return [TagTrace] - containing all the newly generated tags in states with
+     * timestamps
+     */
+    override fun generateTags(
+        wmTrace: WindowManagerTrace,
+        layersTrace: LayersTrace
+    ): TagTrace {
+        val tags = mutableMapOf<Long, MutableList<Tag>>()
+        var currPosition: FSMState? = getInitialState(tags)
+
+        val dumpList = createDumpList(wmTrace, layersTrace)
+        val dumpIterator = dumpList.iterator()
+
+        // keep always a reference to previous, current and next states
+        var previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?
+        var current: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = null
+        var next: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = dumpIterator.next()
+        while (currPosition != null) {
+            previous = current
+            current = next
+            next = if (dumpIterator.hasNext()) dumpIterator.next() else null
+            requireNotNull(current) { "Current state shouldn't be null" }
+            val newPosition = currPosition.process(previous, current, next)
+            currPosition = newPosition
+        }
+
+        return buildTagTrace(tags)
+    }
+
+    private fun buildTagTrace(tags: MutableMap<Long, MutableList<Tag>>): TagTrace {
+        val tagStates = tags.map { entry ->
+            val timestamp = entry.key
+            val stateTags = entry.value
+            TagState(timestamp.toString(), stateTags.toTypedArray())
+        }
+        return TagTrace(tagStates.toTypedArray(), source = "")
+    }
+
+    companion object {
+        internal fun createDumpList(
+            wmTrace: WindowManagerTrace,
+            layersTrace: LayersTrace
+        ): List<DeviceStateDump<WindowManagerState, LayerTraceEntry>> {
+            val wmTimestamps = wmTrace.map { it.timestamp }.toTypedArray()
+            val layersTimestamps = layersTrace.map { it.timestamp }.toTypedArray()
+            val fullTimestamps = setOf(*wmTimestamps, *layersTimestamps).sorted()
+
+            return fullTimestamps.map { baseTimestamp ->
+                val wmState = wmTrace
+                    .lastOrNull { it.timestamp <= baseTimestamp }
+                    ?: wmTrace.first()
+                val layerState = layersTrace
+                    .lastOrNull { it.timestamp <= baseTimestamp }
+                    ?: layersTrace.first()
+                DeviceStateDump(wmState, layerState)
+            }.distinctBy { Pair(it.wmState.timestamp, it.layerState.timestamp) }
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt
new file mode 100644
index 0000000..c5ecc7d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt
@@ -0,0 +1,28 @@
+package com.android.server.wm.traces.common.tags
+
+/**
+ * Tag Class relating to a particular transition event in a WindowManager
+ * or SurfaceFlinger trace state.
+ * @param id The id to match the end and start tags
+ * @param transition Transition the tag represents the transition
+ * @param isStartTag Tag represents the start or end moment in transition
+ * @param layerId The Layer the tag is associated with (or 0 if no taskId associated with it)
+ * @param windowToken The Window the tag is associated
+ * with (or empty string if no taskId associated with it)
+ * @param taskId The Task the tag is associated with (or 0 if no taskId associated with it)
+ */
+data class Tag(
+    val id: Int,
+    val transition: Transition,
+    val isStartTag: Boolean,
+    val layerId: Int = 0,
+    val windowToken: String = "",
+    val taskId: Int = 0
+) {
+    override fun toString(): String {
+        if (isStartTag) {
+            return "Start Of $transition"
+        }
+        return "End Of $transition"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt
new file mode 100644
index 0000000..adfc163
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt
@@ -0,0 +1,37 @@
+package com.android.server.wm.traces.common.tags
+
+import com.android.server.wm.traces.common.ITraceEntry
+import com.android.server.wm.traces.common.prettyTimestamp
+
+/**
+ * Holds the list of tags corresponding to a particular state at a particular time in trace.
+ *
+ * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility
+ *
+ * @param _timestamp Timestamp of the state
+ * @param tags Array of tags contained in the state
+ * @param isFallback False indicate if the tag timestamp was found or true if a default tag is made
+ */
+class TagState(
+    _timestamp: String,
+    val tags: Array<Tag>,
+    val isFallback: Boolean = false
+) : ITraceEntry {
+    override val timestamp: Long = _timestamp.toLong()
+    override fun toString(): String = "FlickerTagState(timestamp=${prettyTimestamp(timestamp)}, " +
+            "tags=$tags)"
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TagState) return false
+        if (timestamp != other.timestamp) return false
+        if (!tags.contentEquals(other.tags)) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = timestamp.hashCode()
+        result = 31 * result + tags.contentDeepHashCode()
+        return result
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt
new file mode 100644
index 0000000..9aa578f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.tags
+
+import com.android.server.wm.traces.common.ITrace
+
+/**
+ * Holds the entire list of [TagState]s representing an entire trace that has been tagged.
+ * @param entries Array of tagged states within the trace
+ * @param source Source of the trace file
+ */
+data class TagTrace(
+    override val entries: Array<TagState>,
+    override val source: String
+) : ITrace<TagState>,
+    List<TagState> by entries.toList() {
+    override fun toString(): String = "FlickerTagTrace(${entries.firstOrNull()?.timestamp ?: 0}, " +
+            "${entries.lastOrNull()?.timestamp ?: 0})"
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TagTrace) return false
+        if (!entries.contentEquals(other.entries)) return false
+        if (source != other.source) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = entries.contentDeepHashCode()
+        result = 31 * result + source.hashCode()
+        return result
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt
new file mode 100644
index 0000000..204d3e2
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.tags
+
+/**
+ * Represents all the possible transitions to be tagged.
+ */
+enum class Transition(private val transitionName: String) {
+    ROTATION("Rotation"),
+    APP_LAUNCH("AppLaunching"),
+    APP_CLOSE("AppClosing"),
+    PIP_ENTER("PipEntering"),
+    PIP_RESIZE("PipResizing"),
+    PIP_EXPAND("PipExpanding"),
+    PIP_EXIT("PipExiting"),
+    IME_APPEAR("ImeAppearing"),
+    IME_DISAPPEAR("ImeDisappearing");
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt
new file mode 100644
index 0000000..d2be4af
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.tags
+
+/**
+ * Saves the information about a transition tag.
+ */
+data class TransitionTag(
+    var tag: Tag,
+    var startTimestamp: Long,
+    var endTimestamp: Long
+) {
+    fun isEmpty(): Boolean {
+        return this.tag.layerId == 0 &&
+            this.tag.taskId == 0 &&
+            this.tag.windowToken.isEmpty()
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt
index 34b7598..ccb31f1 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt
@@ -16,15 +16,13 @@
 
 package com.android.server.wm.traces.common.windowmanager
 
-import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.ITraceEntry
 import com.android.server.wm.traces.common.prettyTimestamp
 import com.android.server.wm.traces.common.windowmanager.windows.Activity
-import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea
 import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent
 import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState
 import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer
-import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask
+import com.android.server.wm.traces.common.windowmanager.windows.Task
 import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
 import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy
 import com.android.server.wm.traces.common.windowmanager.windows.WindowState
@@ -35,6 +33,8 @@
  * This is a generic object that is reused by both Flicker and Winscope and cannot
  * access internal Java/Android functionality
  *
+ * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility
+ *
  **/
 open class WindowManagerState(
     val where: String,
@@ -48,8 +48,9 @@
     val pendingActivities: Array<String>,
     val root: RootWindowContainer,
     val keyguardControllerState: KeyguardControllerState,
-    override val timestamp: Long = 0
+    _timestamp: String = "0"
 ) : ITraceEntry {
+    override val timestamp: Long = _timestamp.toLong()
     val isVisible: Boolean = true
     val stableId: String get() = this::class.simpleName ?: error("Unable to determine class")
     val name: String get() = prettyTimestamp(timestamp)
@@ -65,7 +66,7 @@
         get() = windowContainers.filterIsInstance<DisplayContent>().toTypedArray()
 
     // Stacks in z-order with the top most at the front of the list, starting with primary display.
-    val rootTasks: Array<ActivityTask>
+    val rootTasks: Array<Task>
         get() = displays.flatMap { it.rootTasks.toList() }.toTypedArray()
 
     // Windows in z-order with the top most at the front of the list.
@@ -86,9 +87,17 @@
         get() = windowStates
             .dropWhile { !appWindows.contains(it) }.drop(appWindows.size).toTypedArray()
     val visibleWindows: Array<WindowState>
-        get() = windowStates.filter { it.isSurfaceShown }.toTypedArray()
+        get() = windowStates
+            .filter { it.isVisible }
+            .filter { window ->
+                val activities = getActivitiesForWindow(window.title)
+                val activity = activities.firstOrNull { it.children.contains(window) }
+                activity?.isVisible ?: true
+            }
+            .toTypedArray()
     val topVisibleAppWindow: String
-        get() = appWindows.filter { it.isVisible }
+        get() = visibleWindows
+            .filter { it.isAppWindow }
             .map { it.title }
             .firstOrNull() ?: ""
     val pinnedWindows: Array<WindowState>
@@ -96,6 +105,12 @@
             .filter { it.windowingMode == WINDOWING_MODE_PINNED }
             .toTypedArray()
 
+    /**
+     * Checks if the device state supports rotation, i.e., if the rotation sensor is
+     * enabled (e.g., launcher) and if the rotation not fixed
+     */
+    val canRotate: Boolean
+        get() = policy?.isFixedOrientation != true && policy?.isOrientationNoSensor != true
     val focusedDisplay: DisplayContent? get() = getDisplay(focusedDisplayId)
     val focusedStackId: Int get() = focusedDisplay?.focusedRootTaskId ?: -1
     val focusedActivity: String get() {
@@ -103,46 +118,19 @@
         return if (focusedDisplay != null && focusedDisplay.resumedActivity.isNotEmpty()) {
             focusedDisplay.resumedActivity
         } else {
-            getActivityForWindow(focusedWindow, focusedDisplayId)?.name ?: ""
+            getActivitiesForWindow(focusedWindow, focusedDisplayId).firstOrNull()?.name ?: ""
         }
     }
-    val resumedActivitiesInDisplays: Array<String>
-        get() = displays.flatMap { display ->
-            display.rootTasks.flatMap { it.resumedActivities.toList() }
-        }.toTypedArray()
-    val defaultPinnedStackBounds: Rect
-        get() = displays
-            .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.defaultPinnedStackBounds
-            ?: Rect.EMPTY
-    val pinnedStackMovementBounds: Rect
-        get() = displays
-            .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.pinnedStackMovementBounds
-            ?: Rect.EMPTY
-    val focusedStackActivityType: Int
-        get() = getRootTask(focusedStackId)?.activityType ?: ACTIVITY_TYPE_UNDEFINED
-    val focusedStackWindowingMode: Int
-        get() = getRootTask(focusedStackId)?.windowingMode ?: WINDOWING_MODE_UNDEFINED
     val resumedActivities: Array<String>
         get() = rootTasks.flatMap { it.resumedActivities.toList() }.toTypedArray()
     val resumedActivitiesCount: Int get() = resumedActivities.size
     val stackCount: Int get() = rootTasks.size
-    val displayCount: Int get() = displays.size
-    val homeTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_HOME)?.topTask
-    val recentsTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_RECENTS)?.topTask
+    val homeTask: Task? get() = getStackByActivityType(ACTIVITY_TYPE_HOME)?.topTask
+    val recentsTask: Task? get() = getStackByActivityType(ACTIVITY_TYPE_RECENTS)?.topTask
     val homeActivity: Activity? get() = homeTask?.activities?.lastOrNull()
     val recentsActivity: Activity? get() = recentsTask?.activities?.lastOrNull()
-    val rootTasksCount: Int get() = rootTasks.size
     val isRecentsActivityVisible: Boolean get() = recentsActivity?.isVisible ?: false
-    val dreamTask: ActivityTask?
-        get() = getStackByActivityType(ACTIVITY_TYPE_DREAM)?.topTask
-    val defaultDisplayLastTransition: String get() = getDefaultDisplay()?.lastTransition
-            ?: "Default display not found"
-    val defaultDisplayAppTransitionState: String get() = getDefaultDisplay()?.appTransitionState
-            ?: "Default display not found"
-    val allNavigationBarStates: Array<WindowState>
-        get() = windowStates.filter { it.isValidNavBarType }.toTypedArray()
     val frontWindow: String? get() = windowStates.map { it.title }.firstOrNull()
-    val stableBounds: Rect get() = getDefaultDisplay()?.stableBounds ?: Rect.EMPTY
     val inputMethodWindowState: WindowState?
         get() = getWindowStateForAppToken(inputMethodWindowAppToken)
 
@@ -152,56 +140,6 @@
     fun getDisplay(displayId: Int): DisplayContent? =
         displays.firstOrNull { it.id == displayId }
 
-    fun getTaskDisplayArea(activityName: String): DisplayArea? {
-        val result = displays.mapNotNull { it.getTaskDisplayArea(activityName) }
-
-        if (result.size > 1) {
-            throw IllegalArgumentException(
-                "There must be exactly one activity among all TaskDisplayAreas.")
-        }
-
-        return result.firstOrNull()
-    }
-
-    fun getFrontRootTaskId(displayId: Int): Int =
-        getDisplay(displayId)?.rootTasks?.first()?.rootTaskId ?: 0
-
-    fun getFrontStackActivityType(displayId: Int): Int =
-        getDisplay(displayId)?.rootTasks?.first()?.activityType ?: 0
-
-    fun getFrontStackWindowingMode(displayId: Int): Int =
-        getDisplay(displayId)?.rootTasks?.first()?.windowingMode ?: 0
-
-    fun getTopActivityName(displayId: Int): String {
-        return getDisplay(displayId)
-            ?.rootTasks?.firstOrNull()
-            ?.topTask
-            ?.activities?.firstOrNull()
-            ?.title
-            ?: ""
-    }
-
-    fun getResumedActivitiesCountInPackage(packageName: String): Int {
-        val componentPrefix = "$packageName/"
-        var count = 0
-        displays.forEach { display ->
-            display.rootTasks.forEach { task ->
-                count += task.resumedActivities.count {
-                    it.isNotEmpty() && it.startsWith(componentPrefix)
-                }
-            }
-        }
-        return count
-    }
-
-    fun getResumedActivity(displayId: Int): String {
-        return getDisplay(displayId)?.resumedActivity ?: ""
-    }
-
-    fun containsStack(windowingMode: Int, activityType: Int): Boolean {
-        return countStacks(windowingMode, activityType) > 0
-    }
-
     fun countStacks(windowingMode: Int, activityType: Int): Int {
         var count = 0
         for (stack in rootTasks) {
@@ -216,7 +154,7 @@
         return count
     }
 
-    fun getRootTask(taskId: Int): ActivityTask? =
+    fun getRootTask(taskId: Int): Task? =
         rootTasks.firstOrNull { it.rootTaskId == taskId }
 
     fun getRotation(displayId: Int): Int =
@@ -225,223 +163,53 @@
     fun getOrientation(displayId: Int): Int =
             getDisplay(displayId)?.lastOrientation ?: error("Default display not found")
 
-    fun getStackByActivityType(activityType: Int): ActivityTask? =
+    fun getStackByActivityType(activityType: Int): Task? =
         rootTasks.firstOrNull { it.activityType == activityType }
 
-    fun getStandardStackByWindowingMode(windowingMode: Int): ActivityTask? =
+    fun getStandardStackByWindowingMode(windowingMode: Int): Task? =
         rootTasks.firstOrNull {
             it.activityType == ACTIVITY_TYPE_STANDARD &&
                 it.windowingMode == windowingMode
         }
 
-    fun getStandardTaskCountByWindowingMode(windowingMode: Int): Int {
-        var count = 0
-        for (stack in rootTasks) {
-            if (stack.activityType != ACTIVITY_TYPE_STANDARD) {
-                continue
-            }
-            if (stack.windowingMode == windowingMode) {
-                count += if (stack.tasks.isEmpty()) 1 else stack.tasks.size
-            }
-        }
-        return count
-    }
-
-    /** Get the stack on its display.  */
-    fun getStackByActivity(activityName: String): ActivityTask? {
-        return displays.map { display ->
-            display.rootTasks.reversed().firstOrNull { stack ->
-                stack.containsActivity(activityName)
-            }
-        }.firstOrNull()
-    }
-
     /**
-     * Get the first activity on display with id [displayId], containing a window whose title
+     * Get the all activities on display with id [displayId], containing a window whose title
      * contains [partialWindowTitle]
      *
      * @param partialWindowTitle window title to search
      * @param displayId display where to search the activity
      */
-    fun getActivityForWindow(
+    fun getActivitiesForWindow(
         partialWindowTitle: String,
         displayId: Int = DEFAULT_DISPLAY
-    ): Activity? {
-        return displays.firstOrNull { it.id == displayId }?.rootTasks?.map { stack ->
+    ): List<Activity> {
+        return displays.firstOrNull { it.id == displayId }?.rootTasks?.mapNotNull { stack ->
             stack.getActivity { activity ->
                 activity.hasWindow(partialWindowTitle)
             }
-        }?.firstOrNull()
-    }
-
-    /** Get the stack position on its display. */
-    fun getStackIndexByActivityType(activityType: Int): Int {
-        return displays
-            .map { it.rootTasks.indexOfFirst { p -> p.activityType == activityType } }
-            .firstOrNull { it > -1 }
-            ?: -1
-    }
-
-    /** Get the stack position on its display. */
-    fun getStackIndexByActivity(activityName: String): Int {
-        for (display in displays) {
-            for (i in display.rootTasks.indices.reversed()) {
-                val stack = display.rootTasks[i]
-                if (stack.containsActivity(activityName)) return i
-            }
-        }
-        return -1
-    }
-
-    /** Get display id by activity on it. */
-    fun getDisplayByActivity(activityComponent: String): Int {
-        val task = getTaskByActivity(activityComponent) ?: return -1
-        return getRootTask(task.rootTaskId)?.displayId
-            ?: error("Task with name $activityComponent not found")
+        } ?: emptyList()
     }
 
     fun containsActivity(activityName: String): Boolean =
         rootTasks.any { it.containsActivity(activityName) }
 
-    fun containsNoneOf(activityNames: Iterable<String>): Boolean {
-        for (activityName in activityNames) {
-            for (stack in rootTasks) {
-                if (stack.containsActivity(activityName)) return false
-            }
-        }
-        return true
+    fun isActivityVisible(activityName: String): Boolean {
+        val activity = rootTasks.mapNotNull { it.getActivity(activityName) }.firstOrNull()
+        return activity?.isVisible ?: false
     }
 
-    fun containsActivityInWindowingMode(
-        activityName: String,
-        windowingMode: Int
-    ): Boolean {
-        for (stack in rootTasks) {
-            val activity = stack.getActivity(activityName)
-            if (activity != null && activity.windowingMode == windowingMode) {
-                return true
-            }
-        }
-        return false
-    }
-
-    fun isActivityVisible(activityName: String): Boolean =
-        rootTasks.map { it.getActivity(activityName)?.isVisible ?: false }.firstOrNull()
-            ?: false
-
-    fun isActivityTranslucent(activityName: String): Boolean =
-        rootTasks.map { it.getActivity(activityName)?.isTranslucent ?: false }.firstOrNull()
-            ?: false
-
-    fun isBehindOpaqueActivities(activityName: String): Boolean {
-        for (stack in rootTasks) {
-            val activity = stack.getActivity { a -> a.title == activityName || !a.isTranslucent }
-            if (activity != null) {
-                if (activity.title == activityName) {
-                    return false
-                }
-                if (!activity.isTranslucent) {
-                    return true
-                }
-            }
-        }
-
-        return false
-    }
-
-    fun containsStartedActivities(): Boolean = rootTasks.map {
-        it.getActivity { a -> a.state != STATE_STOPPED && a.state != STATE_DESTROYED } != null
-    }.firstOrNull() ?: false
-
     fun hasActivityState(activityName: String, activityState: String): Boolean =
         rootTasks.any { it.getActivity(activityName)?.state == activityState }
 
-    fun getActivityProcId(activityName: String): Int =
-        rootTasks.mapNotNull { it.getActivity(activityName)?.procId }
-            .firstOrNull()
-            ?: -1
-
-    fun getStackIdByActivity(activityName: String): Int =
-        getTaskByActivity(activityName)?.rootTaskId ?: INVALID_STACK_ID
-
-    fun getTaskByActivity(activityName: String): ActivityTask? =
-        getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED)
-
-    fun getTaskByActivity(activityName: String, windowingMode: Int): ActivityTask? {
-        for (stack in rootTasks) {
-            if (windowingMode == WINDOWING_MODE_UNDEFINED || windowingMode == stack.windowingMode) {
-                val task = stack.getTask { it.getActivity(activityName) != null }
-                if (task != null) {
-                    return task
-                }
-            }
-        }
-        return null
-    }
-
-    /**
-     * Get the number of activities in the task, with the option to count only activities with
-     * specific name.
-     * @param taskId Id of the task where we're looking for the number of activities.
-     * @param activityName Optional name of the activity we're interested in.
-     * @return Number of all activities in the task if activityName is `null`, otherwise will
-     * report number of activities that have specified name.
-     */
-    fun getActivityCountInTask(taskId: Int, activityName: String?): Int {
-        // If activityName is null, count all activities in the task.
-        // Otherwise count activities that have specified name.
-        for (stack in rootTasks) {
-            val task = stack.getTask(taskId) ?: continue
-
-            if (activityName == null) {
-                return task.activities.size
-            }
-            var count = 0
-            for (activity in task.activities) {
-                if (activity.title == activityName) {
-                    count++
-                }
-            }
-            return count
-        }
-        return 0
-    }
-
-    fun getRootTasksCount(displayId: Int): Int {
-        var count = 0
-        for (rootTask in rootTasks) {
-            if (rootTask.displayId == displayId) ++count
-        }
-        return count
-    }
-
     fun pendingActivityContain(activityName: String): Boolean {
         return pendingActivities.contains(activityName)
     }
 
-    fun getMatchingVisibleWindowState(windowName: String): List<WindowState> {
-        return windowStates.filter { it.isSurfaceShown && it.title == windowName }
+    fun getMatchingVisibleWindowState(windowName: String): Array<WindowState> {
+        return windowStates.filter { it.isSurfaceShown && it.title.contains(windowName) }
+                .toTypedArray()
     }
 
-    fun getWindowByPackageName(packageName: String, windowType: Int): WindowState? =
-        getWindowsByPackageName(packageName, windowType).firstOrNull()
-
-    fun getWindowsByPackageName(
-        packageName: String,
-        vararg restrictToTypes: Int
-    ): List<WindowState> =
-        windowStates.filter { ws ->
-            ((ws.title == packageName ||
-                ws.title.startsWith("$packageName/")) &&
-                restrictToTypes.any { type -> type == ws.attributes.type })
-        }
-
-    fun getMatchingWindowType(type: Int): List<WindowState> =
-        windowStates.filter { it.attributes.type == type }
-
-    fun getMatchingWindowTokens(windowName: String): List<String> =
-        windowStates.filter { it.title === windowName }.map { it.token }
-
     fun getNavBarWindow(displayId: Int): WindowState? {
         val navWindow = windowStates.filter { it.isValidNavBarType && it.displayId == displayId }
 
@@ -460,21 +228,13 @@
      * Check if there exists a window record with matching windowName.
      */
     fun containsWindow(windowName: String): Boolean =
-        windowStates.any { it.title == windowName }
+        windowStates.any { it.title.contains(windowName) }
 
     /**
      * Check if at least one window which matches the specified name has shown it's surface.
      */
-    fun isWindowSurfaceShown(windowName: String): Boolean {
-        for (window in windowStates) {
-            if (window.title == windowName) {
-                if (window.isSurfaceShown) {
-                    return true
-                }
-            }
-        }
-        return false
-    }
+    fun isWindowSurfaceShown(windowName: String): Boolean =
+            getMatchingVisibleWindowState(windowName).isNotEmpty()
 
     /**
      * Check if at least one window which matches provided window name is visible.
@@ -494,35 +254,8 @@
         return pinnedWindows.any { it.title.contains(windowName) }
     }
 
-    /**
-     * Checks whether the display contains the given activity.
-     */
-    fun hasActivityInDisplay(displayId: Int, activityName: String): Boolean {
-        for (stack in getDisplay(displayId)!!.rootTasks) {
-            if (stack.containsActivity(activityName)) {
-                return true
-            }
-        }
-        return false
-    }
-
-    fun findFirstWindowWithType(type: Int): WindowState? =
-        windowStates.firstOrNull { it.attributes.type == type }
-
     fun getZOrder(w: WindowState): Int = windowStates.size - windowStates.indexOf(w)
 
-    fun getStandardRootStackByWindowingMode(windowingMode: Int): ActivityTask? {
-        for (task in rootTasks) {
-            if (task.activityType != ACTIVITY_TYPE_STANDARD) {
-                continue
-            }
-            if (task.windowingMode == windowingMode) {
-                return task
-            }
-        }
-        return null
-    }
-
     fun defaultMinimalTaskSize(displayId: Int): Int =
         dpToPx(DEFAULT_RESIZABLE_TASK_SIZE_DP.toFloat(), getDisplay(displayId)!!.dpi)
 
@@ -568,8 +301,10 @@
             !keyguardControllerState.isKeyguardShowing
     }
 
+    fun asTrace(): WindowManagerTrace = WindowManagerTrace(arrayOf(this), source = "")
+
     override fun toString(): String {
-        return prettyTimestamp(timestamp)
+        return "${prettyTimestamp(timestamp)} (timestamp=$timestamp)"
     }
 
     companion object {
@@ -583,10 +318,8 @@
         internal const val ACTIVITY_TYPE_STANDARD = 1
         internal const val DEFAULT_DISPLAY = 0
         internal const val DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP = 440
-        internal const val INVALID_STACK_ID = -1
         internal const val ACTIVITY_TYPE_HOME = 2
         internal const val ACTIVITY_TYPE_RECENTS = 3
-        internal const val ACTIVITY_TYPE_DREAM = 5
         internal const val WINDOWING_MODE_UNDEFINED = 0
         private const val DENSITY_DEFAULT = 160
         /**
@@ -595,7 +328,7 @@
         private const val WINDOWING_MODE_PINNED = 2
 
         /**
-         * @see WindowManager.LayoutParams
+         * @see android.view.WindowManager.LayoutParams
          */
         internal const val TYPE_NAVIGATION_BAR_PANEL = 2024
 
@@ -608,4 +341,7 @@
             return (dp * densityDpi / DENSITY_DEFAULT + 0.5f).toInt()
         }
     }
+    override fun equals(other: Any?): Boolean {
+        return other is WindowManagerState && other.timestamp == this.timestamp
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt
index cd937ac..3517ba9 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt
@@ -28,14 +28,45 @@
  * access internal Java/Android functionality
  *
  */
-open class WindowManagerTrace(
-    override val entries: List<WindowManagerState>,
-    override val source: String,
-    override val sourceChecksum: String
+data class WindowManagerTrace(
+    override val entries: Array<WindowManagerState>,
+    override val source: String
 ) : ITrace<WindowManagerState>,
-    List<WindowManagerState> by entries {
+    List<WindowManagerState> by entries.toList() {
     override fun toString(): String {
         return "WindowManagerTrace(Start: ${entries.first()}, " +
             "End: ${entries.last()})"
     }
-}
\ No newline at end of file
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowManagerTrace) return false
+
+        if (!entries.contentEquals(other.entries)) return false
+        if (source != other.source) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = entries.contentHashCode()
+        result = 31 * result + source.hashCode()
+        return result
+    }
+
+    /**
+     * Split the trace by the start and end timestamp.
+     *
+     * @param from the start timestamp
+     * @param to the end timestamp
+     * @return the subtrace trace(from, to)
+     */
+    fun filter(from: Long, to: Long): WindowManagerTrace {
+        return WindowManagerTrace(
+            this.entries
+                .dropWhile { it.timestamp < from }
+                .dropLastWhile { it.timestamp > to }
+                .toTypedArray(),
+            source = "")
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt
index cff1a7f..5c082a1 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt
@@ -38,10 +38,35 @@
      * @param partialWindowTitle window title to search
      */
     fun hasWindow(partialWindowTitle: String): Boolean {
-        return this.windows.any { it.title.contains(partialWindowTitle) }
+        return collectDescendants<WindowState> { it.title.contains(partialWindowTitle) }
+                .isNotEmpty()
     }
 
     override fun toString(): String {
         return "${this::class.simpleName}: {$token $title} state=$state visible=$isVisible"
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Activity) return false
+
+        if (state != other.state) return false
+        if (frontOfTask != other.frontOfTask) return false
+        if (procId != other.procId) return false
+        if (isTranslucent != other.isTranslucent) return false
+        if (orientation != other.orientation) return false
+        if (title != other.title) return false
+        if (token != other.token) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + state.hashCode()
+        result = 31 * result + frontOfTask.hashCode()
+        result = 31 * result + procId
+        result = 31 * result + isTranslucent.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt
deleted file mode 100644
index 63f80a3..0000000
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2020 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.server.wm.traces.common.windowmanager.windows
-
-import com.android.server.wm.traces.common.Rect
-
-/**
- * Represents a task in the window manager hierarchy
- *
- * This is a generic object that is reused by both Flicker and Winscope and cannot
- * access internal Java/Android functionality
- *
- */
-open class ActivityTask(
-    override val activityType: Int,
-    override val isFullscreen: Boolean,
-    override val bounds: Rect,
-    val taskId: Int,
-    val rootTaskId: Int,
-    val displayId: Int,
-    _lastNonFullscreenBounds: Rect?,
-    val realActivity: String,
-    val origActivity: String,
-    val resizeMode: Int,
-    private val _resumedActivity: String,
-    var animatingBounds: Boolean,
-    val surfaceWidth: Int,
-    val surfaceHeight: Int,
-    val createdByOrganizer: Boolean,
-    val minWidth: Int,
-    val minHeight: Int,
-    windowContainer: WindowContainer
-) : WindowContainer(windowContainer) {
-    override val isVisible: Boolean = false
-    override val name: String = taskId.toString()
-    override val isEmpty: Boolean get() = tasks.isEmpty() && activities.isEmpty()
-
-    val lastNonFullscreenBounds: Rect = _lastNonFullscreenBounds ?: Rect.EMPTY
-    val isRootTask: Boolean get() = taskId == rootTaskId
-    val tasks: List<ActivityTask>
-        get() = this.children.reversed().filterIsInstance<ActivityTask>()
-    val activities: List<Activity>
-        get() = this.children.reversed().filterIsInstance<Activity>()
-    /** The top task in the stack.
-     */
-    // NOTE: Unlike the WindowManager internals, we dump the state from top to bottom,
-    //       so the indices are inverted
-    val topTask: ActivityTask? get() = tasks.firstOrNull()
-    val resumedActivities: Array<String> get() {
-        val result = mutableSetOf<String>()
-        if (this._resumedActivity.isNotEmpty()) {
-            result.add(this._resumedActivity)
-        }
-        val activitiesInChildren = this.tasks
-            .flatMap { it.resumedActivities.toList() }
-            .filter { it.isNotEmpty() }
-        result.addAll(activitiesInChildren)
-        return result.toTypedArray()
-    }
-
-    fun getTask(predicate: (ActivityTask) -> Boolean) =
-        tasks.firstOrNull { predicate(it) } ?: if (predicate(this)) this else null
-
-    fun getTask(taskId: Int) = getTask { t -> t.taskId == taskId }
-
-    fun forAllTasks(consumer: (ActivityTask) -> Any) {
-        tasks.forEach { consumer(it) }
-    }
-
-    fun getActivity(predicate: (Activity) -> Boolean): Activity? {
-        return activities.firstOrNull { predicate(it) }
-            ?: tasks.flatMap { it.activities }
-                .firstOrNull { predicate(it) }
-    }
-
-    fun getActivity(activityName: String): Activity? {
-        return getActivity { activity -> activity.title == activityName }
-    }
-
-    fun containsActivity(activityName: String) = getActivity(activityName) != null
-
-    override fun toString(): String {
-        return "${this::class.simpleName}: {$token $title} id=$taskId bounds=$bounds"
-    }
-}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt
index 44c64f3..13d1f9e 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt
@@ -42,4 +42,32 @@
             smallestScreenWidthDp == 0 &&
             screenLayout == 0 &&
             uiMode == 0
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Configuration) return false
+
+        if (windowConfiguration != other.windowConfiguration) return false
+        if (densityDpi != other.densityDpi) return false
+        if (orientation != other.orientation) return false
+        if (screenHeightDp != other.screenHeightDp) return false
+        if (screenWidthDp != other.screenWidthDp) return false
+        if (smallestScreenWidthDp != other.smallestScreenWidthDp) return false
+        if (screenLayout != other.screenLayout) return false
+        if (uiMode != other.uiMode) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = windowConfiguration?.hashCode() ?: 0
+        result = 31 * result + densityDpi
+        result = 31 * result + orientation
+        result = 31 * result + screenHeightDp
+        result = 31 * result + screenWidthDp
+        result = 31 * result + smallestScreenWidthDp
+        result = 31 * result + screenLayout
+        result = 31 * result + uiMode
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt
index 4ba0b6a..6aacdb8 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt
@@ -42,4 +42,22 @@
         get() = (overrideConfiguration?.isEmpty ?: true) &&
             (fullConfiguration?.isEmpty ?: true) &&
             (mergedOverrideConfiguration?.isEmpty ?: true)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ConfigurationContainer) return false
+
+        if (overrideConfiguration != other.overrideConfiguration) return false
+        if (fullConfiguration != other.fullConfiguration) return false
+        if (mergedOverrideConfiguration != other.mergedOverrideConfiguration) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = overrideConfiguration?.hashCode() ?: 0
+        result = 31 * result + (fullConfiguration?.hashCode() ?: 0)
+        result = 31 * result + (mergedOverrideConfiguration?.hashCode() ?: 0)
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt
index 3067ac6..9c42887 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt
@@ -45,4 +45,23 @@
     override fun toString(): String {
         return "${this::class.simpleName} {$token $title} isTaskArea=$isTaskDisplayArea"
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DisplayArea) return false
+
+        if (isTaskDisplayArea != other.isTaskDisplayArea) return false
+        if (isVisible != other.isVisible) return false
+        if (orientation != other.orientation) return false
+        if (title != other.title) return false
+        if (token != other.token) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + isTaskDisplayArea.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt
index cb21672..6671aee 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt
@@ -30,13 +30,13 @@
     val focusedRootTaskId: Int,
     val resumedActivity: String,
     val singleTaskInstance: Boolean,
-    _defaultPinnedStackBounds: Rect?,
-    _pinnedStackMovementBounds: Rect?,
+    val defaultPinnedStackBounds: Rect,
+    val pinnedStackMovementBounds: Rect,
     val displayRect: Rect,
     val appRect: Rect,
     val dpi: Int,
     val flags: Int,
-    _stableBounds: Rect?,
+    val stableBounds: Rect,
     val surfaceSize: Int,
     val focusedApp: String,
     val lastTransition: String,
@@ -48,16 +48,12 @@
     override val name: String = id.toString()
     override val isVisible: Boolean = false
 
-    val defaultPinnedStackBounds: Rect = _defaultPinnedStackBounds ?: Rect.EMPTY
-    val pinnedStackMovementBounds: Rect = _pinnedStackMovementBounds ?: Rect.EMPTY
-    val stableBounds: Rect = _stableBounds ?: Rect.EMPTY
-
-    val rootTasks: Array<ActivityTask>
+    val rootTasks: Array<Task>
         get() {
-            val tasks = this.collectDescendants<ActivityTask> { it.isRootTask }.toMutableList()
+            val tasks = this.collectDescendants<Task> { it.isRootTask }.toMutableList()
             // TODO(b/149338177): figure out how CTS tests deal with organizer. For now,
             //                    don't treat them as regular stacks
-            val rootOrganizedTasks = mutableListOf<ActivityTask>()
+            val rootOrganizedTasks = mutableListOf<Task>()
             val reversedTaskList = tasks.reversed()
             reversedTaskList.forEach { task ->
                 // Skip tasks created by an organizer
@@ -68,7 +64,7 @@
             }
             // Add root tasks controlled by an organizer
             rootOrganizedTasks.reversed().forEach { task ->
-                tasks.addAll(task.children.reversed().map { it as ActivityTask })
+                tasks.addAll(task.children.reversed().map { it as Task })
             }
 
             return tasks.toTypedArray()
@@ -93,4 +89,55 @@
         return "${this::class.simpleName} #$id: name=$title mDisplayRect=$displayRect " +
             "mAppRect=$appRect mFlags=$flags"
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DisplayContent) return false
+        if (!super.equals(other)) return false
+
+        if (id != other.id) return false
+        if (focusedRootTaskId != other.focusedRootTaskId) return false
+        if (resumedActivity != other.resumedActivity) return false
+        if (defaultPinnedStackBounds != other.defaultPinnedStackBounds) return false
+        if (pinnedStackMovementBounds != other.pinnedStackMovementBounds) return false
+        if (stableBounds != other.stableBounds) return false
+        if (displayRect != other.displayRect) return false
+        if (appRect != other.appRect) return false
+        if (dpi != other.dpi) return false
+        if (flags != other.flags) return false
+        if (focusedApp != other.focusedApp) return false
+        if (lastTransition != other.lastTransition) return false
+        if (appTransitionState != other.appTransitionState) return false
+        if (rotation != other.rotation) return false
+        if (lastOrientation != other.lastOrientation) return false
+        if (name != other.name) return false
+        if (singleTaskInstance != other.singleTaskInstance) return false
+        if (surfaceSize != other.surfaceSize) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + id
+        result = 31 * result + focusedRootTaskId
+        result = 31 * result + resumedActivity.hashCode()
+        result = 31 * result + singleTaskInstance.hashCode()
+        result = 31 * result + defaultPinnedStackBounds.hashCode()
+        result = 31 * result + pinnedStackMovementBounds.hashCode()
+        result = 31 * result + displayRect.hashCode()
+        result = 31 * result + appRect.hashCode()
+        result = 31 * result + dpi
+        result = 31 * result + flags
+        result = 31 * result + stableBounds.hashCode()
+        result = 31 * result + surfaceSize
+        result = 31 * result + focusedApp.hashCode()
+        result = 31 * result + lastTransition.hashCode()
+        result = 31 * result + appTransitionState.hashCode()
+        result = 31 * result + rotation
+        result = 31 * result + lastOrientation
+        result = 31 * result + name.hashCode()
+        result = 31 * result + isVisible.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Task.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Task.kt
new file mode 100644
index 0000000..1c17405
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Task.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2020 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Rect
+
+/**
+ * Represents a task in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class Task(
+    override val activityType: Int,
+    override val isFullscreen: Boolean,
+    override val bounds: Rect,
+    val taskId: Int,
+    val rootTaskId: Int,
+    val displayId: Int,
+    val lastNonFullscreenBounds: Rect,
+    val realActivity: String,
+    val origActivity: String,
+    val resizeMode: Int,
+    private val _resumedActivity: String,
+    var animatingBounds: Boolean,
+    val surfaceWidth: Int,
+    val surfaceHeight: Int,
+    val createdByOrganizer: Boolean,
+    val minWidth: Int,
+    val minHeight: Int,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    override val isVisible: Boolean = false
+    override val name: String = taskId.toString()
+    override val isEmpty: Boolean get() = tasks.isEmpty() && activities.isEmpty()
+    override val stableId: String get() = "${super.stableId} $taskId"
+
+    val isRootTask: Boolean get() = taskId == rootTaskId
+    val tasks: Array<Task>
+        get() = this.children.reversed().filterIsInstance<Task>().toTypedArray()
+    val taskFragments: Array<TaskFragment>
+        get() = this.children.reversed().filterIsInstance<TaskFragment>().toTypedArray()
+    val activities: Array<Activity>
+        get() = this.children.reversed().filterIsInstance<Activity>().toTypedArray()
+    /** The top task in the stack.
+     */
+    // NOTE: Unlike the WindowManager internals, we dump the state from top to bottom,
+    //       so the indices are inverted
+    val topTask: Task? get() = tasks.firstOrNull()
+    val resumedActivities: Array<String> get() {
+        val result = mutableSetOf<String>()
+        if (this._resumedActivity.isNotEmpty()) {
+            result.add(this._resumedActivity)
+        }
+        val activitiesInChildren = this.tasks
+            .flatMap { it.resumedActivities.toList() }
+            .filter { it.isNotEmpty() }
+        result.addAll(activitiesInChildren)
+        return result.toTypedArray()
+    }
+
+    fun getTask(predicate: (Task) -> Boolean) =
+        tasks.firstOrNull { predicate(it) } ?: if (predicate(this)) this else null
+
+    fun getTask(taskId: Int) = getTask { t -> t.taskId == taskId }
+
+    fun getActivityWithTask(predicate: (Task, Activity) -> Boolean): Activity? {
+        return activities.firstOrNull { predicate(this, it) }
+            ?: tasks.flatMap { task -> task.activities.filter { predicate(task, it) } }
+                .firstOrNull()
+    }
+
+    fun forAllTasks(consumer: (Task) -> Any) {
+        tasks.forEach { consumer(it) }
+    }
+
+    fun getActivity(predicate: (Activity) -> Boolean): Activity? {
+        return activities.firstOrNull { predicate(it) }
+            ?: tasks.flatMap { it.activities.toList() }
+                .firstOrNull { predicate(it) }
+    }
+
+    fun getActivity(activityName: String): Activity? {
+        return getActivity { activity -> activity.title.contains(activityName) }
+    }
+
+    fun containsActivity(activityName: String) = getActivity(activityName) != null
+
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title} id=$taskId bounds=$bounds"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Task) return false
+
+        if (activityType != other.activityType) return false
+        if (isFullscreen != other.isFullscreen) return false
+        if (bounds != other.bounds) return false
+        if (taskId != other.taskId) return false
+        if (rootTaskId != other.rootTaskId) return false
+        if (displayId != other.displayId) return false
+        if (realActivity != other.realActivity) return false
+        if (resizeMode != other.resizeMode) return false
+        if (minWidth != other.minWidth) return false
+        if (minHeight != other.minHeight) return false
+        if (name != other.name) return false
+        if (orientation != other.orientation) return false
+        if (title != other.title) return false
+        if (token != other.token) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + activityType
+        result = 31 * result + isFullscreen.hashCode()
+        result = 31 * result + bounds.hashCode()
+        result = 31 * result + taskId
+        result = 31 * result + rootTaskId
+        result = 31 * result + displayId
+        result = 31 * result + lastNonFullscreenBounds.hashCode()
+        result = 31 * result + realActivity.hashCode()
+        result = 31 * result + origActivity.hashCode()
+        result = 31 * result + resizeMode
+        result = 31 * result + _resumedActivity.hashCode()
+        result = 31 * result + animatingBounds.hashCode()
+        result = 31 * result + surfaceWidth
+        result = 31 * result + surfaceHeight
+        result = 31 * result + createdByOrganizer.hashCode()
+        result = 31 * result + minWidth
+        result = 31 * result + minHeight
+        result = 31 * result + isVisible.hashCode()
+        result = 31 * result + name.hashCode()
+        return result
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt
new file mode 100644
index 0000000..d18daa0
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents a task fragment in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class TaskFragment(
+    override val activityType: Int,
+    val displayId: Int,
+    val minWidth: Int,
+    val minHeight: Int,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    val tasks: Array<Task>
+        get() = this.children.reversed().filterIsInstance<Task>().toTypedArray()
+    val taskFragments: Array<TaskFragment>
+        get() = this.children.reversed().filterIsInstance<TaskFragment>().toTypedArray()
+    val activities: Array<Activity>
+        get() = this.children.reversed().filterIsInstance<Activity>().toTypedArray()
+
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title} bounds=$bounds"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TaskFragment) return false
+
+        if (activityType != other.activityType) return false
+        if (displayId != other.displayId) return false
+        if (minWidth != other.minWidth) return false
+        if (minHeight != other.minHeight) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + activityType
+        result = 31 * result + displayId
+        result = 31 * result + minWidth
+        result = 31 * result + minHeight
+        return result
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt
index ffdda82..c2eb894 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt
@@ -42,4 +42,26 @@
             maxBounds.isEmpty &&
             windowingMode == 0 &&
             activityType == 0
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowConfiguration) return false
+
+        if (windowingMode != other.windowingMode) return false
+        if (activityType != other.activityType) return false
+        if (appBounds != other.appBounds) return false
+        if (bounds != other.bounds) return false
+        if (maxBounds != other.maxBounds) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = windowingMode
+        result = 31 * result + activityType
+        result = 31 * result + appBounds.hashCode()
+        result = 31 * result + bounds.hashCode()
+        result = 31 * result + maxBounds.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt
index db48e0d..b81f2ab 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt
@@ -31,6 +31,7 @@
     val title: String,
     val token: String,
     val orientation: Int,
+    val layerId: Int,
     _isVisible: Boolean,
     configurationContainer: ConfigurationContainer,
     val children: Array<WindowContainer>
@@ -43,6 +44,7 @@
         titleOverride ?: windowContainer.title,
         windowContainer.token,
         windowContainer.orientation,
+        windowContainer.layerId,
         isVisibleOverride ?: windowContainer.isVisible,
         windowContainer,
         windowContainer.children
@@ -50,13 +52,10 @@
 
     open val isVisible: Boolean = _isVisible
     open val name: String = title
-    val stableId: String get() = "${this::class.simpleName} $token $title"
+    open val stableId: String get() = "${this::class.simpleName} $token $title"
     open val isFullscreen: Boolean = false
     open val bounds: Rect = Rect.EMPTY
 
-    val windows: Array<WindowState>
-        get() = this.collectDescendants()
-
     fun traverseTopDown(): List<WindowContainer> {
         val traverseList = mutableListOf(this)
 
@@ -111,6 +110,33 @@
         return name
     }
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowContainer) return false
+
+        if (title != other.title) return false
+        if (token != other.token) return false
+        if (orientation != other.orientation) return false
+        if (isVisible != other.isVisible) return false
+        if (name != other.name) return false
+        if (isFullscreen != other.isFullscreen) return false
+        if (bounds != other.bounds) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = title.hashCode()
+        result = 31 * result + token.hashCode()
+        result = 31 * result + orientation
+        result = 31 * result + children.contentHashCode()
+        result = 31 * result + isVisible.hashCode()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + isFullscreen.hashCode()
+        result = 31 * result + bounds.hashCode()
+        return result
+    }
+
     override val isEmpty: Boolean
         get() = super.isEmpty &&
             title.isEmpty() &&
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt
index def86db..9c73e6d 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt
@@ -63,4 +63,78 @@
          */
         private const val TYPE_NAVIGATION_BAR = 2019
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowLayoutParams) return false
+
+        if (type != other.type) return false
+        if (x != other.x) return false
+        if (y != other.y) return false
+        if (width != other.width) return false
+        if (height != other.height) return false
+        if (horizontalMargin != other.horizontalMargin) return false
+        if (verticalMargin != other.verticalMargin) return false
+        if (gravity != other.gravity) return false
+        if (softInputMode != other.softInputMode) return false
+        if (format != other.format) return false
+        if (windowAnimations != other.windowAnimations) return false
+        if (alpha != other.alpha) return false
+        if (screenBrightness != other.screenBrightness) return false
+        if (buttonBrightness != other.buttonBrightness) return false
+        if (rotationAnimation != other.rotationAnimation) return false
+        if (preferredRefreshRate != other.preferredRefreshRate) return false
+        if (preferredDisplayModeId != other.preferredDisplayModeId) return false
+        if (hasSystemUiListeners != other.hasSystemUiListeners) return false
+        if (inputFeatureFlags != other.inputFeatureFlags) return false
+        if (userActivityTimeout != other.userActivityTimeout) return false
+        if (colorMode != other.colorMode) return false
+        if (flags != other.flags) return false
+        if (privateFlags != other.privateFlags) return false
+        if (systemUiVisibilityFlags != other.systemUiVisibilityFlags) return false
+        if (subtreeSystemUiVisibilityFlags != other.subtreeSystemUiVisibilityFlags) return false
+        if (appearance != other.appearance) return false
+        if (behavior != other.behavior) return false
+        if (fitInsetsTypes != other.fitInsetsTypes) return false
+        if (fitInsetsSides != other.fitInsetsSides) return false
+        if (fitIgnoreVisibility != other.fitIgnoreVisibility) return false
+        if (isValidNavBarType != other.isValidNavBarType) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = type
+        result = 31 * result + x
+        result = 31 * result + y
+        result = 31 * result + width
+        result = 31 * result + height
+        result = 31 * result + horizontalMargin.hashCode()
+        result = 31 * result + verticalMargin.hashCode()
+        result = 31 * result + gravity
+        result = 31 * result + softInputMode
+        result = 31 * result + format
+        result = 31 * result + windowAnimations
+        result = 31 * result + alpha.hashCode()
+        result = 31 * result + screenBrightness.hashCode()
+        result = 31 * result + buttonBrightness.hashCode()
+        result = 31 * result + rotationAnimation
+        result = 31 * result + preferredRefreshRate.hashCode()
+        result = 31 * result + preferredDisplayModeId
+        result = 31 * result + hasSystemUiListeners.hashCode()
+        result = 31 * result + inputFeatureFlags
+        result = 31 * result + userActivityTimeout.hashCode()
+        result = 31 * result + colorMode
+        result = 31 * result + flags
+        result = 31 * result + privateFlags
+        result = 31 * result + systemUiVisibilityFlags
+        result = 31 * result + subtreeSystemUiVisibilityFlags
+        result = 31 * result + appearance
+        result = 31 * result + behavior
+        result = 31 * result + fitInsetsTypes
+        result = 31 * result + fitInsetsSides
+        result = 31 * result + fitIgnoreVisibility.hashCode()
+        result = 31 * result + isValidNavBarType.hashCode()
+        return result
+    }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt
index 2bc52d0..cc06fa5 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt
@@ -37,4 +37,78 @@
     val rotationMode: Int,
     val screenOnFully: Boolean,
     val windowManagerDrawComplete: Boolean
-)
\ No newline at end of file
+) {
+    val isOrientationNoSensor: Boolean
+        get() = orientation == SCREEN_ORIENTATION_NOSENSOR
+
+    val isFixedOrientation: Boolean
+        get() = isFixedOrientationLandscape ||
+            isFixedOrientationPortrait ||
+            orientation == SCREEN_ORIENTATION_LOCKED
+
+    private val isFixedOrientationLandscape
+        get() = orientation == SCREEN_ORIENTATION_LANDSCAPE ||
+            orientation == SCREEN_ORIENTATION_SENSOR_LANDSCAPE ||
+            orientation == SCREEN_ORIENTATION_REVERSE_LANDSCAPE ||
+            orientation == SCREEN_ORIENTATION_USER_LANDSCAPE
+
+    private val isFixedOrientationPortrait
+        get() = orientation == SCREEN_ORIENTATION_PORTRAIT ||
+            orientation == SCREEN_ORIENTATION_SENSOR_PORTRAIT ||
+            orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT ||
+            orientation == SCREEN_ORIENTATION_USER_PORTRAIT
+
+    companion object {
+        /**
+         * From [android.content.pm.ActivityInfo]
+         */
+        private const val SCREEN_ORIENTATION_LANDSCAPE = 0
+        private const val SCREEN_ORIENTATION_PORTRAIT = 1
+        private const val SCREEN_ORIENTATION_NOSENSOR = 5
+        private const val SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6
+        private const val SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7
+        private const val SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8
+        private const val SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9
+        private const val SCREEN_ORIENTATION_USER_LANDSCAPE = 11
+        private const val SCREEN_ORIENTATION_USER_PORTRAIT = 12
+        private const val SCREEN_ORIENTATION_LOCKED = 14
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowManagerPolicy) return false
+
+        if (focusedAppToken != other.focusedAppToken) return false
+        if (forceStatusBar != other.forceStatusBar) return false
+        if (forceStatusBarFromKeyguard != other.forceStatusBarFromKeyguard) return false
+        if (keyguardDrawComplete != other.keyguardDrawComplete) return false
+        if (keyguardOccluded != other.keyguardOccluded) return false
+        if (keyguardOccludedChanged != other.keyguardOccludedChanged) return false
+        if (keyguardOccludedPending != other.keyguardOccludedPending) return false
+        if (lastSystemUiFlags != other.lastSystemUiFlags) return false
+        if (orientation != other.orientation) return false
+        if (rotation != other.rotation) return false
+        if (rotationMode != other.rotationMode) return false
+        if (screenOnFully != other.screenOnFully) return false
+        if (windowManagerDrawComplete != other.windowManagerDrawComplete) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = focusedAppToken.hashCode()
+        result = 31 * result + forceStatusBar.hashCode()
+        result = 31 * result + forceStatusBarFromKeyguard.hashCode()
+        result = 31 * result + keyguardDrawComplete.hashCode()
+        result = 31 * result + keyguardOccluded.hashCode()
+        result = 31 * result + keyguardOccludedChanged.hashCode()
+        result = 31 * result + keyguardOccludedPending.hashCode()
+        result = 31 * result + lastSystemUiFlags
+        result = 31 * result + orientation
+        result = 31 * result + rotation
+        result = 31 * result + rotationMode
+        result = 31 * result + screenOnFully.hashCode()
+        result = 31 * result + windowManagerDrawComplete.hashCode()
+        return result
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt
index d85e860..f11bfc8 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt
@@ -16,7 +16,7 @@
 
 package com.android.server.wm.traces.common.windowmanager.windows
 
-import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.Size
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.Region
 
@@ -34,31 +34,22 @@
     val layer: Int,
     val isSurfaceShown: Boolean,
     val windowType: Int,
-    val requestedSize: Bounds,
+    val requestedSize: Size,
     val surfacePosition: Rect?,
-    _frame: Rect?,
-    _containingFrame: Rect?,
-    _parentFrame: Rect?,
-    _contentFrame: Rect?,
-    _contentInsets: Rect?,
-    _surfaceInsets: Rect?,
-    _givenContentInsets: Rect?,
-    _crop: Rect?,
+    val frame: Rect,
+    val containingFrame: Rect,
+    val parentFrame: Rect,
+    val contentFrame: Rect,
+    val contentInsets: Rect,
+    val surfaceInsets: Rect,
+    val givenContentInsets: Rect,
+    val crop: Rect,
     windowContainer: WindowContainer,
     val isAppWindow: Boolean
 ) : WindowContainer(windowContainer, getWindowTitle(windowContainer.title)) {
     override val isVisible: Boolean get() = super.isVisible && attributes.alpha > 0
 
-    val frame: Rect = _frame ?: Rect.EMPTY
-    val containingFrame: Rect = _containingFrame ?: Rect.EMPTY
-    val parentFrame: Rect = _parentFrame ?: Rect.EMPTY
-    val contentFrame: Rect = _contentFrame ?: Rect.EMPTY
-    val contentInsets: Rect = _contentInsets ?: Rect.EMPTY
-    val surfaceInsets: Rect = _surfaceInsets ?: Rect.EMPTY
-    val givenContentInsets: Rect = _givenContentInsets ?: Rect.EMPTY
-    val crop: Rect = _crop ?: Rect.EMPTY
     override val isFullscreen: Boolean get() = this.attributes.flags.and(FLAG_FULLSCREEN) > 0
-
     val isStartingWindow: Boolean = windowType == WINDOW_TYPE_STARTING
     val isExitingWindow: Boolean = windowType == WINDOW_TYPE_EXITING
     val isDebuggerWindow: Boolean = windowType == WINDOW_TYPE_DEBUGGER
@@ -79,13 +70,29 @@
         "type=${attributes.type} cf=$containingFrame pf=$parentFrame"
 
     override fun equals(other: Any?): Boolean {
-        return other is WindowState &&
-            other.stableId == stableId &&
-            other.attributes == attributes &&
-            other.token == token &&
-            other.title == title &&
-            other.containingFrame == containingFrame &&
-            other.parentFrame == parentFrame
+        if (this === other) return true
+        if (other !is WindowState) return false
+
+        if (name != other.name) return false
+        if (attributes != other.attributes) return false
+        if (displayId != other.displayId) return false
+        if (stackId != other.stackId) return false
+        if (layer != other.layer) return false
+        if (isSurfaceShown != other.isSurfaceShown) return false
+        if (windowType != other.windowType) return false
+        if (requestedSize != other.requestedSize) return false
+        if (surfacePosition != other.surfacePosition) return false
+        if (frame != other.frame) return false
+        if (containingFrame != other.containingFrame) return false
+        if (parentFrame != other.parentFrame) return false
+        if (contentFrame != other.contentFrame) return false
+        if (contentInsets != other.contentInsets) return false
+        if (surfaceInsets != other.surfaceInsets) return false
+        if (givenContentInsets != other.givenContentInsets) return false
+        if (crop != other.crop) return false
+        if (isAppWindow != other.isAppWindow) return false
+
+        return true
     }
 
     override fun hashCode(): Int {
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt
index cfcaafc..9d2b911 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt
@@ -28,4 +28,20 @@
     override fun toString(): String {
         return "${this::class.simpleName}: {$token $title}"
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WindowToken) return false
+        if (!super.equals(other)) return false
+
+        if (isVisible != other.isVisible) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + isVisible.hashCode()
+        return result
+    }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt
deleted file mode 100644
index be03b2d..0000000
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2020 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.server.wm.traces.parser
-
-import android.os.SystemClock
-import android.util.Log
-
-/**
- * The utility class to wait a condition with customized options.
- * The default retry policy is 5 times with interval 1 second.
- *
- * @param <T> The type of the object to validate.
- *
- * <p>Sample:</p>
- * <pre>
- * // Simple case.
- * if (Condition.waitFor("true value", () -> true)) {
- *     println("Success");
- * }
- * // Wait for customized result with customized validation.
- * String result = Condition.waitForResult(new Condition<String>("string comparison")
- *         .setResultSupplier(() -> "Result string")
- *         .setResultValidator(str -> str.equals("Expected string"))
- *         .setRetryIntervalMs(500)
- *         .setRetryLimit(3)
- *         .setOnFailure(str -> println("Failed on " + str)));
- * </pre>
- */
-class Condition<T>
-/**
- * Constructs with a simple boolean condition.
- *
- * When satisfier = null, it is expected that the condition will be configured with
- * [.setResultSupplier] and [.setResultValidator].
- *
- * @param message The message to show what is waiting for.
- * @param satisfier If it returns true, that means the condition is satisfied.
- */
-@JvmOverloads constructor(
-    private var message: String = "",
-    /**
-     * It decides whether this condition is satisfied.
-     */
-    private var satisfier: (() -> Boolean)? = null,
-    private var retryLimit: Int = DEFAULT_RETRY_LIMIT,
-    private var retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS
-) {
-    private var returnLastResult: Boolean = false
-
-    /**
-     * It is used when the condition is not a simple boolean expression, such as the caller may
-     * want to get the validated product as the return value.
-     */
-    private var resultSupplier: (() -> T?)? = null
-
-    /**
-     * It validates the result from [.mResultSupplier].
-     */
-    private var resultValidator: ((T?) -> Boolean)? = null
-    private var onFailure: ((T) -> Any)? = null
-    private var onRetry: Runnable? = null
-    private var lastResult: T? = null
-    private var validatedResult: T? = null
-
-    /**
-     * Set the supplier which provides the result object to validate.
-     */
-    fun setResultSupplier(supplier: () -> T?): Condition<T> =
-        apply { resultSupplier = supplier }
-
-    /**
-     * Set the validator which tests the object provided by the supplier.
-     */
-    fun setResultValidator(validator: (T?) -> Boolean): Condition<T> =
-        apply { resultValidator = validator }
-
-    /**
-     * If true, when using [.waitForResult], the method will return the last result
-     * provided by [.mResultSupplier] even it is not valid (by [.mResultValidator]).
-     */
-    fun setReturnLastResult(returnLastResult: Boolean): Condition<T> =
-        apply { this.returnLastResult = returnLastResult }
-
-    /**
-     * Executes the action when the condition does not satisfy within the time limit. The passed
-     * object to the consumer will be the last result from the supplier.
-     */
-    fun setOnFailure(onFailure: (T) -> Any): Condition<T> = apply { this.onFailure = onFailure }
-
-    fun setOnRetry(onRetry: Runnable): Condition<T> = apply { this.onRetry = onRetry }
-
-    fun setRetryIntervalMs(millis: Long): Condition<T> = apply { retryIntervalMs = millis }
-
-    fun setRetryLimit(limit: Int): Condition<T> = apply { retryLimit = limit }
-
-    /**
-     * Build the condition by [.mResultSupplier] and [.mResultValidator].
-     */
-    private fun prepareSatisfier(): () -> Boolean {
-        val supplier = resultSupplier
-        val validator = resultValidator
-        require(!(supplier == null || validator == null)) { "No specified condition" }
-
-        return {
-            val result = supplier.invoke()
-            lastResult = result
-            if (validator.invoke(result)) {
-                validatedResult = result
-                true
-            } else {
-                false
-            }
-        }.also {
-            satisfier = it
-        }
-    }
-
-    companion object {
-        // TODO(b/112837428): Implement a incremental retry policy to reduce the unnecessary
-        // constant time, currently keep the default as 5*1s because most of the original code
-        // uses it, and some tests might be sensitive to the waiting interval.
-        private const val DEFAULT_RETRY_LIMIT = 5
-        private const val DEFAULT_RETRY_INTERVAL_MS = 1000L
-
-        /**
-         * @see .waitFor
-         * @see .Condition
-         */
-        @JvmStatic
-        @JvmOverloads
-        fun <T> waitFor(
-            message: String,
-            retryLimit: Int = DEFAULT_RETRY_LIMIT,
-            retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS,
-            satisfier: () -> Boolean
-        ): Boolean {
-            val condition = Condition<T>(message, satisfier, retryLimit, retryIntervalMs)
-            return waitFor(condition)
-        }
-
-        /**
-         * @return `false` if the condition does not satisfy within the time limit.
-         */
-        @JvmStatic
-        fun <T> waitFor(condition: Condition<T>): Boolean {
-            val satisfier = condition.satisfier ?: condition.prepareSatisfier()
-            val startTime = SystemClock.elapsedRealtime()
-            Log.v(LOG_TAG, "***Waiting for ${condition.message}")
-            for (i in 1..condition.retryLimit) {
-                if (satisfier.invoke()) {
-                    Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!")
-                    return true
-                } else {
-                    SystemClock.sleep(condition.retryIntervalMs)
-                    Log.v(LOG_TAG, "***Waiting for ${condition.message} ... retry=$i" +
-                        " elapsed=${SystemClock.elapsedRealtime() - startTime} ms")
-                    val onRetry = condition.onRetry
-                    if (onRetry != null && i < condition.retryLimit) {
-                        onRetry.run()
-                    }
-                }
-            }
-            if (satisfier.invoke()) {
-                Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!")
-                return true
-            }
-            val onFailure = condition.onFailure
-            if (onFailure == null) {
-                Log.e(LOG_TAG, "***Waiting for ${condition.message} ... Failed!")
-            } else {
-                val result = condition.lastResult
-                require(result != null) { "Missing last result for failure notification" }
-                onFailure.invoke(result)
-            }
-            return false
-        }
-
-        /**
-         * @see .waitForResult
-         */
-        @JvmStatic
-        fun <T> waitForResult(message: String, setup: (Condition<T>) -> Any): T? {
-            val condition = Condition<T>(message)
-            setup.invoke(condition)
-            return waitForResult(condition)
-        }
-
-        /**
-         * @return `null` if the condition does not satisfy within the time limit or the result
-         * supplier returns `null`.
-         */
-        @JvmStatic
-        fun <T> waitForResult(condition: Condition<T>): T? {
-            condition.validatedResult = null
-            condition.lastResult = condition.validatedResult
-            condition.prepareSatisfier()
-            waitFor(condition)
-            return when {
-                condition.validatedResult != null -> condition.validatedResult
-                condition.returnLastResult -> condition.lastResult
-                else -> null
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceDumpParser.kt
similarity index 82%
rename from libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
rename to libraries/flicker/src/com/android/server/wm/traces/parser/DeviceDumpParser.kt
index 6534b31..d47bdbc 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceDumpParser.kt
@@ -16,6 +16,8 @@
 
 package com.android.server.wm.traces.parser
 
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.DeviceTraceDump
 import com.android.server.wm.traces.common.layers.LayersTrace
 import com.android.server.wm.traces.common.layers.LayerTraceEntry
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
@@ -27,16 +29,7 @@
  * Represents a state dump containing the [WindowManagerTrace] and the [LayersTrace] both parsed
  * and in raw (byte) data.
  */
-class DeviceStateDump(
-    /**
-     * Parsed [WindowManagerTrace]
-     */
-    val wmTrace: WindowManagerTrace?,
-    /**
-     * Parsed [LayersTrace]
-     */
-    val layersTrace: LayersTrace?
-) {
+class DeviceDumpParser {
     companion object {
         /**
          * Creates a device state dump containing the [WindowManagerTrace] and [LayersTrace]
@@ -47,15 +40,18 @@
          * @param layersTraceData [LayersTrace] content
          */
         @JvmStatic
-        fun fromDump(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
+        fun fromDump(
+            wmTraceData: ByteArray,
+            layersTraceData: ByteArray
+        ): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> {
             return DeviceStateDump(
-                wmTrace = if (wmTraceData.isNotEmpty()) {
-                    WindowManagerTraceParser.parseFromDump(wmTraceData)
+                wmState = if (wmTraceData.isNotEmpty()) {
+                    WindowManagerTraceParser.parseFromDump(wmTraceData).first()
                 } else {
                     null
                 },
-                layersTrace = if (layersTraceData.isNotEmpty()) {
-                    LayersTraceParser.parseFromDump(layersTraceData)
+                layerState = if (layersTraceData.isNotEmpty()) {
+                    LayersTraceParser.parseFromTrace(layersTraceData).first()
                 } else {
                     null
                 }
@@ -71,8 +67,8 @@
          * @param layersTraceData [LayersTrace] content
          */
         @JvmStatic
-        fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
-            return DeviceStateDump(
+        fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceTraceDump {
+            return DeviceTraceDump(
                 wmTrace = if (wmTraceData.isNotEmpty()) {
                     WindowManagerTraceParser.parseFromTrace(wmTraceData)
                 } else {
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
index bb20b5c..32f2225 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
@@ -22,8 +22,12 @@
 import android.content.ComponentName
 import android.os.ParcelFileDescriptor
 import android.util.Log
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 
 internal const val LOG_TAG = "AMWM_FLICKER"
 
@@ -35,9 +39,33 @@
     return android.graphics.Rect(left, top, right, bottom)
 }
 
-fun ComponentName.toActivityName(): String = this.flattenToShortString()
+/**
+ * Subtracts [other] region from this [this] region
+ */
+fun Region.minus(other: Region): android.graphics.Region = minus(other.toAndroidRegion())
 
-fun ComponentName.toWindowName(): String = this.flattenToString()
+/**
+ * Subtracts [other] region from this [this] region
+ */
+fun Region.minus(other: android.graphics.Region): android.graphics.Region {
+    val thisRegion = this.toAndroidRegion()
+    thisRegion.op(other, android.graphics.Region.Op.XOR)
+    return thisRegion
+}
+
+/**
+ * Adds [other] region to this [this] region
+ */
+fun Region.plus(other: Region): android.graphics.Region = plus(other.toAndroidRegion())
+
+/**
+ * Adds [other] region to this [this] region
+ */
+fun Region.plus(other: android.graphics.Region): android.graphics.Region {
+    val thisRegion = this.toAndroidRegion()
+    thisRegion.op(other, android.graphics.Region.Op.XOR)
+    return thisRegion
+}
 
 private fun executeCommand(uiAutomation: UiAutomation, cmd: String): ByteArray {
     val fileDescriptor = uiAutomation.executeShellCommand(cmd)
@@ -80,9 +108,15 @@
 fun getCurrentStateDump(
     uiAutomation: UiAutomation,
     @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
-): DeviceStateDump {
+): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> {
     val currentStateDump = getCurrentState(uiAutomation, dumpFlags)
     val wmTraceData = currentStateDump.first
     val layersTraceData = currentStateDump.second
-    return DeviceStateDump.fromDump(wmTraceData, layersTraceData)
+    return DeviceDumpParser.fromDump(wmTraceData, layersTraceData)
 }
+
+/**
+ * Converts an Android [ComponentName] into a flicker [FlickerComponentName]
+ */
+fun ComponentName.toFlickerComponent(): FlickerComponentName =
+    FlickerComponentName(this.packageName, this.className)
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt
new file mode 100644
index 0000000..e91b956
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.parser.errors
+
+import android.util.Log
+import com.android.server.wm.flicker.FlickerErrorTraceProto
+import com.android.server.wm.traces.common.errors.Error
+import com.android.server.wm.traces.common.errors.ErrorState
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.parser.LOG_TAG
+import java.nio.file.Path
+import kotlin.system.measureTimeMillis
+
+/**
+ * Class that holds the methods to parse error proto files into error classes.
+ */
+class ErrorTraceParserUtil {
+    companion object {
+        /**
+         * Parses [FlickerErrorTraceProto] from [data] and uses the proto to generates a list
+         * of trace entries, storing the flattened layers into its hierarchical structure.
+         *
+         * @param data binary proto data
+         * @param source Path to source of data for additional debug information
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            data: ByteArray,
+            source: Path? = null
+        ): ErrorTrace {
+            var fileProto: FlickerErrorTraceProto? = null
+            try {
+                measureTimeMillis {
+                    fileProto = FlickerErrorTraceProto.parseFrom(data)
+                }.also {
+                    Log.v(LOG_TAG, "Parsing proto (Flicker Errors Trace): ${it}ms")
+                }
+            } catch (e: Exception) {
+                throw RuntimeException(e)
+            }
+            return fileProto?.let {
+                parseFromTrace(it, source)
+            } ?: error("Unable to read flicker errors trace file")
+        }
+
+        /**
+         * Parses [FlickerErrorTraceProto] from [proto] and uses the proto to generates a list
+         * of trace entries, storing the flattened layers into its hierarchical structure.
+         *
+         * @param proto Parsed proto data
+         * @param source Path to source of data for additional debug information
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            proto: FlickerErrorTraceProto,
+            source: Path? = null
+        ): ErrorTrace {
+            val states = mutableListOf<ErrorState>()
+            var traceParseTime = 0L
+            for (stateProto in proto.statesList) {
+                val errorParseTime = measureTimeMillis {
+                    val errors = mutableListOf<Error>()
+                    for (errorProto in stateProto.errorsList) {
+                        errors.add(
+                                Error(
+                                    stacktrace = errorProto.stacktrace,
+                                    message = errorProto.message,
+                                    layerId = errorProto.layerId,
+                                    windowToken = errorProto.windowToken,
+                                    taskId = errorProto.taskId,
+                                    assertionName = errorProto.assertionName
+                                ))
+                    }
+                    states.add(
+                            ErrorState(
+                                _timestamp = stateProto.timestamp.toString(),
+                                errors = errors.toTypedArray()))
+                }
+                traceParseTime += errorParseTime
+            }
+            return ErrorTrace(
+                    entries = states.toTypedArray(),
+                    source = source?.toString() ?: ""
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt
new file mode 100644
index 0000000..0d183ae
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:JvmName("Extensions")
+
+package com.android.server.wm.traces.parser.errors
+
+import android.util.Log
+import com.android.server.wm.flicker.FlickerErrorProto
+import com.android.server.wm.flicker.FlickerErrorStateProto
+import com.android.server.wm.flicker.FlickerErrorTraceProto
+import com.android.server.wm.traces.common.errors.ErrorState
+import com.android.server.wm.traces.common.errors.ErrorTrace
+import com.android.server.wm.traces.common.errors.Error
+import com.android.server.wm.traces.parser.LOG_TAG
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Stores the error trace in a .winscope file.
+ */
+fun ErrorTrace.writeToFile(outputFile: Path) {
+    val proto = FlickerErrorTraceProto
+        .newBuilder()
+        .addAllStates(this.entries.map { it.toProto() })
+        .setMagicNumber(
+            FlickerErrorTraceProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+                FlickerErrorTraceProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+        )
+        .build()
+    val errorTraceBytes = proto.toByteArray()
+
+    try {
+        Log.d(LOG_TAG, outputFile.toString())
+        Files.createDirectories(outputFile.parent)
+        Files.write(outputFile, errorTraceBytes)
+    } catch (e: IOException) {
+        throw RuntimeException("Unable to create error trace file: ${e.message}", e)
+    }
+}
+
+fun ErrorState.toProto(): FlickerErrorStateProto = FlickerErrorStateProto
+    .newBuilder()
+    .addAllErrors(this.errors.map { it.toProto() })
+    .setTimestamp(this.timestamp)
+    .build()
+
+fun Error.toProto(): FlickerErrorProto = FlickerErrorProto
+    .newBuilder()
+    .setStacktrace(this.stacktrace)
+    .setMessage(this.message)
+    .setLayerId(this.layerId)
+    .setWindowToken(this.windowToken)
+    .setTaskId(this.taskId)
+    .setAssertionName(this.assertionName)
+    .build()
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt
index 5bf48bf..bd473e5 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt
@@ -17,8 +17,10 @@
 package com.android.server.wm.traces.parser.layers
 
 import android.graphics.Rect
+import android.surfaceflinger.nano.Common.RectProto
+import android.surfaceflinger.nano.Common.SizeProto
+import android.surfaceflinger.nano.Display.DisplayProto
 import android.surfaceflinger.nano.Layers
-import android.surfaceflinger.nano.Layers.RectProto
 import android.surfaceflinger.nano.Layers.RegionProto
 import android.surfaceflinger.nano.Layerstrace
 import android.util.Log
@@ -26,6 +28,8 @@
 import com.android.server.wm.traces.common.Color
 import com.android.server.wm.traces.common.RectF
 import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.Size
+import com.android.server.wm.traces.common.layers.Display
 import com.android.server.wm.traces.common.layers.Layer
 import com.android.server.wm.traces.common.layers.LayerTraceEntry
 import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder
@@ -33,6 +37,7 @@
 import com.android.server.wm.traces.parser.LOG_TAG
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException
 import java.nio.file.Path
+import kotlin.math.max
 import kotlin.system.measureTimeMillis
 
 /**
@@ -46,7 +51,6 @@
          *
          * @param data binary proto data
          * @param source Path to source of data for additional debug information
-         * @param sourceChecksum Checksum of the source file
          * @param orphanLayerCallback a callback to handle any unexpected orphan layers
          */
         @JvmOverloads
@@ -54,10 +58,9 @@
         fun parseFromTrace(
             data: ByteArray,
             source: Path? = null,
-            sourceChecksum: String = "",
             orphanLayerCallback: ((Layer) -> Boolean)? = null
         ): LayersTrace {
-            val fileProto: Layerstrace.LayersTraceFileProto
+            var fileProto: Layerstrace.LayersTraceFileProto? = null
             try {
                 measureTimeMillis {
                     fileProto = Layerstrace.LayersTraceFileProto.parseFrom(data)
@@ -67,7 +70,9 @@
             } catch (e: Exception) {
                 throw RuntimeException(e)
             }
-            return parseFromTrace(fileProto, source, sourceChecksum, orphanLayerCallback)
+            return fileProto?.let {
+                parseFromTrace(it, source, orphanLayerCallback)
+            } ?: error("Unable to read trace file")
         }
 
         /**
@@ -76,7 +81,6 @@
          *
          * @param proto Parsed proto data
          * @param source Path to source of data for additional debug information
-         * @param sourceChecksum Checksum of the source file
          * @param orphanLayerCallback a callback to handle any unexpected orphan layers
          */
         @JvmOverloads
@@ -84,7 +88,6 @@
         fun parseFromTrace(
             proto: Layerstrace.LayersTraceFileProto,
             source: Path? = null,
-            sourceChecksum: String = "",
             orphanLayerCallback: ((Layer) -> Boolean)? = null
         ): LayersTrace {
             val entries: MutableList<LayerTraceEntry> = ArrayList()
@@ -92,15 +95,16 @@
             for (traceProto: Layerstrace.LayersTraceProto in proto.entry) {
                 val entryParseTime = measureTimeMillis {
                     val entry = newEntry(
-                        traceProto.elapsedRealtimeNanos, traceProto.layers.layers,
-                        traceProto.hwcBlob, traceProto.where, orphanLayerCallback)
+                        traceProto.elapsedRealtimeNanos, traceProto.displays,
+                        traceProto.layers.layers, traceProto.hwcBlob, traceProto.where,
+                        orphanLayerCallback)
                     entries.add(entry)
                 }
                 traceParseTime += entryParseTime
             }
             Log.v(LOG_TAG, "Parsing duration (Layers Trace): ${traceParseTime}ms " +
-                "(avg ${traceParseTime / entries.size}ms per entry)")
-            return LayersTrace(entries, source?.toString() ?: "", sourceChecksum)
+                "(avg ${traceParseTime / max(entries.size, 1)}ms per entry)")
+            return LayersTrace(entries.toTypedArray(), source?.toString() ?: "")
         }
 
         /**
@@ -110,8 +114,11 @@
          * @param proto Parsed proto data
          */
         @JvmStatic
-        fun parseFromDump(proto: Layers.LayersProto): LayersTrace {
-            val entry = newEntry(timestamp = 0, protos = proto.layers)
+        @Deprecated("This functions parsers old SF dumps. Now SF dumps create a " +
+            "single entry trace, for new dump use [parseFromTrace]")
+        fun parseFromLegacyDump(proto: Layers.LayersProto): LayersTrace {
+            val entry = newEntry(timestamp = 0, displayProtos = emptyArray(),
+                protos = proto.layers)
             return LayersTrace(entry)
         }
 
@@ -122,25 +129,29 @@
          * @param data binary proto data
          */
         @JvmStatic
-        fun parseFromDump(data: ByteArray?): LayersTrace {
+        @Deprecated("This functions parsers old SF dumps. Now SF dumps create a " +
+            "single entry trace, for new dump use [parseFromTrace]")
+        fun parseFromLegacyDump(data: ByteArray?): LayersTrace {
             val traceProto = try {
                 Layers.LayersProto.parseFrom(data)
             } catch (e: InvalidProtocolBufferNanoException) {
                 throw RuntimeException(e)
             }
-            return parseFromDump(traceProto)
+            return parseFromLegacyDump(traceProto)
         }
 
         @JvmStatic
         private fun newEntry(
             timestamp: Long,
+            displayProtos: Array<DisplayProto>,
             protos: Array<Layers.LayerProto>,
             hwcBlob: String = "",
             where: String = "",
             orphanLayerCallback: ((Layer) -> Boolean)? = null
         ): LayerTraceEntry {
-            val layers = protos.map { newLayer(it) }
-            val builder = LayerTraceEntryBuilder(timestamp, layers, hwcBlob, where)
+            val layers = protos.map { newLayer(it) }.toTypedArray()
+            val displays = displayProtos.map { newDisplay(it) }.toTypedArray()
+            val builder = LayerTraceEntryBuilder(timestamp, layers, displays, hwcBlob, where)
             builder.setOrphanLayerCallback(orphanLayerCallback)
             return builder.build()
         }
@@ -171,7 +182,7 @@
                     proto.type ?: "",
                     proto.screenBounds?.toRectF(),
                     Transform(proto.transform, proto.position),
-                    proto.sourceBounds?.toRectF(),
+                    proto.sourceBounds?.toRectF() ?: RectF.EMPTY,
                     proto.currFrame,
                     proto.effectiveScalingMode,
                     Transform(proto.bufferTransform, position = null),
@@ -185,6 +196,17 @@
             )
         }
 
+        private fun newDisplay(proto: DisplayProto): Display {
+            return Display(
+                proto.id.toULong(),
+                proto.name,
+                proto.layerStack,
+                proto.size.toSize(),
+                proto.layerStackSpaceRect.toRect(),
+                Transform(proto.transform, position = null)
+            )
+        }
+
         @JvmStatic
         private fun Layers.FloatRectProto?.toRectF(): RectF? {
             return this?.let {
@@ -193,6 +215,13 @@
         }
 
         @JvmStatic
+        private fun SizeProto?.toSize(): Size {
+            return this?.let {
+                Size(this.w, this.h)
+            } ?: Size.EMPTY
+        }
+
+        @JvmStatic
         private fun Layers.ColorProto?.toColor(): Color {
             if (this == null) {
                 return Color.EMPTY
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt
index ca1b28f..856ebb2 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.traces.parser.layers
 
 import android.surfaceflinger.nano.Layers
+import android.surfaceflinger.nano.Common.TransformProto
 import com.android.server.wm.traces.common.layers.Transform
 import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_H_VAL
 import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_V_VAL
@@ -26,13 +27,13 @@
 import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagClear
 import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet
 
-class Transform(transform: Layers.TransformProto?, position: Layers.PositionProto?) :
+class Transform(transform: TransformProto?, position: Layers.PositionProto?) :
         Transform(
             transform?.type,
             getMatrix(transform, position)
         )
 
-private fun getMatrix(transform: Layers.TransformProto?, position: Layers.PositionProto?):
+private fun getMatrix(transform: TransformProto?, position: Layers.PositionProto?):
         Transform.Matrix {
     val x = position?.x ?: 0f
     val y = position?.y ?: 0f
@@ -65,4 +66,4 @@
         else ->
             throw IllegalStateException("Unknown transform type $this")
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt
new file mode 100644
index 0000000..2877580
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:JvmName("Extensions")
+
+package com.android.server.wm.traces.parser.tags
+
+import android.util.Log
+import com.android.server.wm.flicker.FlickerTagProto
+import com.android.server.wm.flicker.FlickerTagStateProto
+import com.android.server.wm.flicker.FlickerTagTraceProto
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.TagState
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.parser.LOG_TAG
+import java.nio.file.Files
+import java.nio.file.Path
+
+fun TagTrace.writeToFile(outputFile: Path) {
+    val proto = FlickerTagTraceProto
+        .newBuilder()
+        .addAllStates(this.entries.map { it.toProto() })
+        .setMagicNumber(
+            FlickerTagTraceProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+                FlickerTagTraceProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+        )
+        .build()
+
+    val tagTraceBytes = proto.toByteArray()
+
+    try {
+        Log.d(LOG_TAG, outputFile.toString())
+        Files.createDirectories(outputFile.parent)
+        Files.write(outputFile, tagTraceBytes)
+    } catch (e: Exception) {
+        throw RuntimeException("Unable to create error trace file: ${e.message}", e)
+    }
+}
+
+fun TagState.toProto(): FlickerTagStateProto = FlickerTagStateProto
+    .newBuilder()
+    .addAllTags(this.tags.map { it.toProto() })
+    .setTimestamp(this.timestamp)
+    .build()
+
+fun Tag.toProto(): FlickerTagProto = FlickerTagProto
+    .newBuilder()
+    .setIsStartTag(this.isStartTag)
+    .setTransition(FlickerTagProto.Transition.valueOf(this.transition.name))
+    .setId(this.id)
+    .setTaskId(this.taskId)
+    .setWindowToken(this.windowToken)
+    .setLayerId(this.layerId)
+    .build()
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt
new file mode 100644
index 0000000..1dd346c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 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.server.wm.traces.parser.tags
+
+import android.util.Log
+import com.android.server.wm.flicker.FlickerTagTraceProto
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.TagState
+import com.android.server.wm.traces.common.tags.TagTrace
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.parser.LOG_TAG
+import java.nio.file.Path
+import kotlin.system.measureTimeMillis
+
+/**
+ * Class that holds the methods to parse tag proto files into tag classes.
+ */
+class TagTraceParserUtil {
+    companion object {
+        /**
+         * Parses [FlickerTagTraceProto] from [data] and uses the proto to generates a list
+         * of trace entries, storing the flattened layers into its hierarchical structure.
+         *
+         * @param data binary proto data
+         * @param source Path to source of data for additional debug information
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            data: ByteArray,
+            source: Path? = null
+        ): TagTrace {
+            var fileProto: FlickerTagTraceProto? = null
+            try {
+                measureTimeMillis {
+                    fileProto = FlickerTagTraceProto.parseFrom(data)
+                }.also {
+                    Log.v(LOG_TAG, "Parsing proto (Flicker Tags Trace): ${it}ms")
+                }
+            } catch (e: Exception) {
+                throw RuntimeException(e)
+            }
+            return fileProto?.let {
+                parseFromTrace(it)
+            } ?: error("Unable to read flicker tags trace file")
+        }
+
+        /**
+         * Parses [FlickerTagTraceProto] from [proto] and uses the proto to generates a list
+         * of trace entries, storing the flattened layers into its hierarchical structure.
+         *
+         * @param proto Parsed proto data
+         * @param source Path to source of data for additional debug informationy
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            proto: FlickerTagTraceProto,
+            source: Path? = null
+        ): TagTrace {
+            val states = mutableListOf<TagState>()
+            var traceParseTime = 0L
+            for (stateProto in proto.statesList) {
+                val tagParseTime = measureTimeMillis {
+                    val tags = mutableListOf<Tag>()
+                    for (tagProto in stateProto.tagsList) {
+                        tags.add(
+                            Tag(
+                                layerId = tagProto.layerId,
+                                windowToken = tagProto.windowToken,
+                                taskId = tagProto.taskId,
+                                transition = Transition.valueOf(tagProto.transition.name),
+                                id = tagProto.id,
+                                isStartTag = tagProto.isStartTag
+                        ))
+                    }
+                    states.add(
+                            TagState(
+                                    _timestamp = stateProto.timestamp.toString(),
+                                    tags = tags.toTypedArray()))
+                }
+                traceParseTime += tagParseTime
+            }
+            return TagTrace(
+                    entries = states.toTypedArray(),
+                    source = source?.toString() ?: ""
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt
index 1a0b23c..8889fd0 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt
@@ -18,13 +18,11 @@
 
 import android.app.ActivityTaskManager
 import android.app.WindowConfiguration
-import android.content.ComponentName
-import com.android.server.wm.traces.parser.toActivityName
-import com.android.server.wm.traces.parser.toWindowName
+import com.android.server.wm.traces.common.FlickerComponentName
 
 data class WaitForValidActivityState(
     @JvmField
-    val activityName: ComponentName?,
+    val activityName: FlickerComponentName?,
     @JvmField
     val windowName: String?,
     @JvmField
@@ -34,7 +32,7 @@
     @JvmField
     val activityType: Int
 ) {
-    constructor(activityName: ComponentName) : this(
+    constructor(activityName: FlickerComponentName) : this(
         activityName,
         windowName = activityName.toWindowName(),
         stackId = ActivityTaskManager.INVALID_STACK_ID,
@@ -70,7 +68,7 @@
         return sb.toString()
     }
 
-    class Builder constructor(internal var activityName: ComponentName? = null) {
+    class Builder constructor(internal var activityName: FlickerComponentName? = null) {
         internal var windowName: String? = activityName?.toWindowName()
         internal var stackId = ActivityTaskManager.INVALID_STACK_ID
         internal var windowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt
index 3d4b083..522915b 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt
@@ -19,25 +19,26 @@
 import android.app.ActivityTaskManager
 import android.app.Instrumentation
 import android.app.WindowConfiguration
-import android.content.ComponentName
 import android.graphics.Rect
 import android.graphics.Region
 import android.os.SystemClock
 import android.util.Log
 import android.view.Display
-import androidx.annotation.VisibleForTesting
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.traces.common.layers.LayerTraceEntry
-import com.android.server.wm.traces.common.windowmanager.WindowManagerState
-import com.android.server.wm.traces.parser.getCurrentStateDump
 import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer
 import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
 import com.android.server.wm.traces.common.windowmanager.windows.WindowState
-import com.android.server.wm.traces.parser.Condition
+import com.android.server.wm.traces.common.Condition
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.FlickerComponentName.Companion.IME
 import com.android.server.wm.traces.parser.LOG_TAG
-import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.common.WaitCondition
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.parser.getCurrentStateDump
 import com.android.server.wm.traces.parser.toAndroidRegion
-import com.android.server.wm.traces.parser.toWindowName
 
 open class WindowManagerStateHelper @JvmOverloads constructor(
     /**
@@ -47,53 +48,51 @@
     /**
      * Predicate to supply a new UI information
      */
-    private val deviceDumpSupplier: () -> Dump = {
-        val currState = getCurrentStateDump(
-            instrumentation.uiAutomation)
-        Dump(
-            currState.wmTrace?.entries?.first() ?: error("Unable to parse WM trace"),
-            currState.layersTrace?.entries?.first() ?: error("Unable to parse Layers trace")
+    private val deviceDumpSupplier: () -> DeviceStateDump<WindowManagerState, LayerTraceEntry> = {
+        val currState = getCurrentStateDump(instrumentation.uiAutomation)
+        DeviceStateDump(
+            currState.wmState ?: error("Unable to parse WM trace"),
+            currState.layerState ?: error("Unable to parse Layers trace")
         )
     },
     /**
      * Number of attempts to satisfy a wait condition
      */
-    private val numRetries: Int = 5,
+    private val numRetries: Int = WaitCondition.DEFAULT_RETRY_LIMIT,
     /**
      * Interval between wait for state dumps during wait conditions
      */
-    private val retryIntervalMs: Long = 500L
+    private val retryIntervalMs: Long = WaitCondition.DEFAULT_RETRY_INTERVAL_MS
 ) {
-    /**
-     * Fetches the current device state
-     */
-    val currentState: Dump
-        get() = computeState(ignoreInvalidStates = true)
+    private var internalState: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = null
 
     /**
      * Queries the supplier for a new device state
-     *
-     * @param ignoreInvalidStates If false, retries up to [numRetries] times (with a sleep
-     * interval of [retryIntervalMs] ms to obtain a complete WM state, otherwise returns the
-     * first state
      */
-    protected open fun computeState(ignoreInvalidStates: Boolean = false): Dump {
-        var newState = deviceDumpSupplier.invoke()
-        for (retryNr in 0..numRetries) {
-            val wmState = newState.wmState
-            if (!ignoreInvalidStates && wmState.isIncomplete()) {
-                Log.w(LOG_TAG, "***Incomplete AM state: ${wmState.getIsIncompleteReason()}" +
-                    " Waiting ${retryIntervalMs}ms and retrying ($retryNr/$numRetries)...")
-                SystemClock.sleep(retryIntervalMs)
-                newState = deviceDumpSupplier.invoke()
+    val currentState: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+        get() {
+            if (internalState == null) {
+                internalState = deviceDumpSupplier.invoke()
             } else {
-                break
+                waitForValidState()
             }
-        }
-
-        return newState
+            return internalState ?: error("Unable to fetch an internal state")
     }
 
+    protected open fun updateCurrState(
+        value: DeviceStateDump<WindowManagerState, LayerTraceEntry>
+    ) {
+        internalState = value
+    }
+
+    private fun createConditionBuilder():
+        WaitCondition.Builder<DeviceStateDump<WindowManagerState, LayerTraceEntry>> =
+        WaitCondition.Builder(deviceDumpSupplier, numRetries)
+            .onSuccess { updateCurrState(it) }
+            .onFailure { updateCurrState(it) }
+            .onLog { Log.d(LOG_TAG, it) }
+            .onRetry { SystemClock.sleep(retryIntervalMs) }
+
     private fun ConfigurationContainer.isWindowingModeCompatible(
         requestedWindowingMode: Int
     ): Boolean {
@@ -111,16 +110,15 @@
      * @param waitForActivitiesVisible array of activity states to wait for.
      */
     fun waitForValidState(vararg waitForActivitiesVisible: WaitForValidActivityState): Boolean {
-        val success = Condition.waitFor<WindowManagerState>("valid stacks and activities states",
-            retryLimit = numRetries, retryIntervalMs = retryIntervalMs) {
-            // TODO: Get state of AM and WM at the same time to avoid mismatches caused by
-            // requesting dump in some intermediate state.
-            val state = computeState()
-            !(shouldWaitForValidityCheck(state) ||
-                shouldWaitForValidStacks(state) ||
-                shouldWaitForActivities(state, *waitForActivitiesVisible) ||
-                shouldWaitForWindows(state))
+        val builder = createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isWMStateComplete())
+
+        if (waitForActivitiesVisible.isNotEmpty()) {
+            builder.withCondition("!shouldWaitForActivities") {
+                !shouldWaitForActivities(it, *waitForActivitiesVisible)
+            }
         }
+        val success = builder.build().waitFor()
         if (!success) {
             Log.e(LOG_TAG, "***Waiting for states failed: " +
                 waitForActivitiesVisible.contentToString())
@@ -128,225 +126,140 @@
         return success
     }
 
-    fun waitForFullScreenApp(componentName: ComponentName): Boolean =
+    fun waitForFullScreenApp(component: FlickerComponentName): Boolean =
             waitForValidState(
                     WaitForValidActivityState
-                            .Builder(componentName)
+                            .Builder(component)
                             .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
                             .setActivityType(WindowConfiguration.ACTIVITY_TYPE_STANDARD)
                             .build())
 
-    fun waitForHomeActivityVisible(): Boolean {
-        return waitFor { it.wmState.homeActivity?.isVisible == true } &&
-            waitForNavBarStatusBarVisible() &&
-            waitForAppTransitionIdle()
-    }
+    fun waitForHomeActivityVisible(): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isHomeActivityVisible())
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .withCondition(WindowManagerConditionsFactory.isNavBarVisible())
+            .withCondition(WindowManagerConditionsFactory.isStatusBarVisible())
+            .build()
+            .waitFor()
 
     fun waitForRecentsActivityVisible(): Boolean =
-        waitFor("recents activity to be visible") {
-            it.wmState.isRecentsActivityVisible
-        }
-
-    fun waitForAodShowing(): Boolean =
-        waitFor("AOD showing") {
-            it.wmState.keyguardControllerState.isAodShowing
-        }
-
-    fun waitForKeyguardGone(): Boolean =
-        waitFor("Keyguard gone") {
-            !it.wmState.keyguardControllerState.isKeyguardShowing
-        }
+        createConditionBuilder()
+            .withCondition("isRecentsActivityVisible") {
+                it.wmState.isRecentsActivityVisible
+            }
+            .build()
+            .waitFor()
 
     /**
      * Wait for specific rotation for the default display. Values are Surface#Rotation
      */
     @JvmOverloads
-    fun waitForRotation(rotation: Int, displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
-        waitFor("Rotation: $rotation") {
-            val currRotation = it.wmState.getRotation(displayId)
-            val rotationLayerExists = it.layerState.isVisible(ROTATION_LAYER_NAME)
-            val blackSurfaceLayerExists = it.layerState.isVisible(BLACK_SURFACE_LAYER_NAME)
-            val anyLayerAnimating = it.layerState.visibleLayers.any { layer ->
-                !layer.transform.isSimpleRotation
+    fun waitForRotation(rotation: Int, displayId: Int = Display.DEFAULT_DISPLAY): Boolean {
+        val hasRotationCondition = WindowManagerConditionsFactory.hasRotation(rotation, displayId)
+        return createConditionBuilder()
+            .withCondition("waitForRotation[$rotation]") {
+                if (!it.wmState.canRotate) {
+                    Log.v(LOG_TAG, "Rotation is not allowed in the state")
+                    true
+                } else {
+                    hasRotationCondition.isSatisfied(it)
+                }
             }
-            Log.v(LOG_TAG, "currRotation($currRotation) " +
-                "anyLayerAnimating($anyLayerAnimating) " +
-                "blackSurfaceLayerExists($blackSurfaceLayerExists) " +
-                "rotationLayerExists($rotationLayerExists)")
-            currRotation == rotation &&
-                !anyLayerAnimating &&
-                !rotationLayerExists &&
-                !blackSurfaceLayerExists
-        }
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
+    }
 
-    /**
-     * Wait for specific orientation for the default display.
-     * Values are ActivityInfo.ScreenOrientation
-     */
-    @JvmOverloads
-    fun waitForLastOrientation(
-        orientation: Int,
-        displayId: Int = Display.DEFAULT_DISPLAY
-    ): Boolean =
-        waitFor("LastOrientation: $orientation") {
-            val result = it.wmState.getOrientation(displayId)
-            Log.v(LOG_TAG, "Current: $result Expected: $orientation")
-            result == orientation
-        }
-
-    fun waitForActivityState(activity: ComponentName, activityState: String): Boolean {
+    fun waitForActivityState(activity: FlickerComponentName, activityState: String): Boolean {
         val activityName = activity.toActivityName()
-        return waitFor("state of $activityName to be $activityState") {
-            it.wmState.hasActivityState(activityName, activityState)
-        }
+        return createConditionBuilder()
+            .withCondition("state of $activityName to be $activityState") {
+                it.wmState.hasActivityState(activityName, activityState)
+            }
+            .build()
+            .waitFor()
     }
 
     /**
      * Waits until the navigation and status bars are visible (windows and layers)
      */
     fun waitForNavBarStatusBarVisible(): Boolean =
-        waitFor("Navigation and Status bar to be visible") {
-            val navBarWindowVisible = it.wmState.isWindowVisible(NAV_BAR_WINDOW_NAME)
-            val statusBarWindowVisible = it.wmState.isWindowVisible(STATUS_BAR_WINDOW_NAME)
-            val navBarLayerVisible = it.layerState.isVisible(NAV_BAR_LAYER_NAME)
-            val navBarLayerAlpha = it.layerState.getLayerWithBuffer(NAV_BAR_LAYER_NAME)
-                ?.color?.a ?: 0f
-            val statusBarLayerVisible = it.layerState.isVisible(STATUS_BAR_LAYER_NAME)
-            val statusBarLayerAlpha = it.layerState.getLayerWithBuffer(STATUS_BAR_LAYER_NAME)
-                ?.color?.a ?: 0f
-            val result = navBarWindowVisible &&
-                navBarLayerVisible &&
-                statusBarWindowVisible &&
-                statusBarLayerVisible &&
-                navBarLayerAlpha == 1f &&
-                statusBarLayerAlpha == 1f
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isNavBarVisible())
+            .withCondition(WindowManagerConditionsFactory.isStatusBarVisible())
+            .build()
+            .waitFor()
 
-            Log.v(LOG_TAG, "Current $result " +
-                "navBarWindowVisible($navBarWindowVisible) " +
-                "navBarLayerVisible($navBarLayerVisible) " +
-                "statusBarWindowVisible($statusBarWindowVisible) " +
-                "statusBarLayerVisible($statusBarLayerVisible) " +
-                "navBarLayerAlpha($navBarLayerAlpha) " +
-                "statusBarLayerAlpha($statusBarLayerAlpha)")
+    fun waitForVisibleWindow(component: FlickerComponentName): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.containsActivity(component))
+            .withCondition(WindowManagerConditionsFactory.containsWindow(component))
+            .withCondition(WindowManagerConditionsFactory.isActivityVisible(component))
+            .withCondition(WindowManagerConditionsFactory.isWindowSurfaceShown(component))
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
 
-            result
-        }
-
-    fun waitForVisibleWindow(activity: ComponentName): Boolean {
-        val activityName = activity.toActivityName()
-        val windowName = activity.toWindowName()
-        return waitFor("$activityName to exist") {
-            val containsActivity = it.wmState.containsActivity(activityName)
-            val containsWindow = it.wmState.containsWindow(windowName)
-            val activityVisible = containsActivity && it.wmState.isActivityVisible(activityName)
-            val windowVisible = containsWindow && it.wmState.isWindowSurfaceShown(windowName)
-            val result = containsActivity &&
-                containsWindow &&
-                activityVisible &&
-                windowVisible
-
-            Log.v(LOG_TAG, "Current: $result " +
-                "containsActivity($containsActivity) " +
-                "containsWindow($containsWindow) " +
-                "activityVisible($activityVisible) " +
-                "windowVisible($windowVisible)")
-
-            result
-        }
-    }
-
-    fun waitForActivityRemoved(activity: ComponentName): Boolean {
-        val activityName = activity.toActivityName()
-        val windowName = activity.toWindowName()
-        return waitFor("$activityName to be removed") {
-            val containsActivity = it.wmState.containsActivity(activityName)
-            val containsWindow = it.wmState.containsWindow(windowName)
-            val result = !containsActivity && !containsWindow
-
-            Log.v(LOG_TAG, "Current: $result" +
-                "containsActivity($containsActivity)" +
-                "containsWindow($containsWindow)")
-            result
-        }
-    }
-
-    fun waitForPendingActivityContain(activity: ComponentName): Boolean {
-        val activityName: String = activity.toActivityName()
-        return waitFor("$activityName in pending list") {
-            it.wmState.pendingActivityContain(activityName)
-        }
-    }
+    fun waitForActivityRemoved(component: FlickerComponentName): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.containsActivity(component).negate())
+            .withCondition(WindowManagerConditionsFactory.containsWindow(component).negate())
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
 
     @JvmOverloads
     fun waitForAppTransitionIdle(displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
-        waitFor("app transition idle on Display $displayId") {
-            val result =
-                it.wmState.getDisplay(displayId)?.appTransitionState
-            Log.v(LOG_TAG, "Current: $result")
-            WindowManagerState.APP_STATE_IDLE == result
-        }
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isAppTransitionIdle(displayId))
+            .build()
+            .waitFor()
 
-    fun waitForWindowSurfaceDisappeared(componentName: ComponentName): Boolean {
-        val windowName = componentName.toWindowName()
-        return waitFor("$windowName's surface is disappeared") {
-            !it.wmState.isWindowSurfaceShown(windowName)
-        }
+    fun waitForWindowSurfaceDisappeared(component: FlickerComponentName): Boolean {
+        val condition = WindowManagerConditionsFactory.isWindowSurfaceShown(component).negate()
+        return createConditionBuilder()
+            .withCondition(condition)
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
     }
 
-    fun waitForSurfaceAppeared(surfaceName: String): Boolean {
-        return waitFor("$surfaceName surface is appeared") {
-            it.wmState.isWindowSurfaceShown(surfaceName)
-        }
-    }
+    fun waitForSurfaceAppeared(surfaceName: String): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isWindowSurfaceShown(surfaceName))
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
 
-    fun waitWindowingModeTopFocus(
-        windowingMode: Int,
-        topFocus: Boolean,
-        message: String
-    ): Boolean = waitFor(message) {
-        val stack = it.wmState.getStandardStackByWindowingMode(windowingMode)
-        (stack != null && topFocus == (it.wmState.focusedStackId == stack.rootTaskId))
+    fun waitFor(
+        vararg conditions: Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>>
+    ): Boolean {
+        val builder = createConditionBuilder()
+        conditions.forEach { builder.withCondition(it) }
+        return builder.build().waitFor()
     }
 
     @JvmOverloads
     fun waitFor(
         message: String = "",
-        waitCondition: (Dump) -> Boolean
-    ): Boolean = Condition.waitFor<Dump>(message, retryLimit = numRetries,
-        retryIntervalMs = retryIntervalMs) {
-        val state = computeState()
-        waitCondition.invoke(state)
-    }
-
-    /**
-     * @return true if should wait for valid stacks state.
-     */
-    private fun shouldWaitForValidStacks(state: Dump): Boolean {
-        if (state.wmState.stackCount == 0) {
-            Log.i(LOG_TAG, "***stackCount=0")
-            return true
-        }
-        if (!state.wmState.keyguardControllerState.isKeyguardShowing &&
-            state.wmState.resumedActivities.isEmpty()) {
-            if (!state.wmState.keyguardControllerState.isKeyguardShowing) {
-                Log.i(LOG_TAG, "***resumedActivitiesCount=0")
-            } else {
-                Log.i(LOG_TAG, "***isKeyguardShowing=true")
-            }
-            return true
-        }
-        if (state.wmState.focusedActivity.isEmpty()) {
-            Log.i(LOG_TAG, "***focusedActivity=null")
-            return true
-        }
-        return false
-    }
+        waitCondition: (DeviceStateDump<WindowManagerState, LayerTraceEntry>) -> Boolean
+    ): Boolean = createConditionBuilder()
+            .withCondition(message, waitCondition)
+            .build()
+            .waitFor()
 
     /**
      * @return true if should wait for some activities to become visible.
      */
     private fun shouldWaitForActivities(
-        state: Dump,
+        state: DeviceStateDump<WindowManagerState, LayerTraceEntry>,
         vararg waitForActivitiesVisible: WaitForValidActivityState
     ): Boolean {
         if (waitForActivitiesVisible.isEmpty()) {
@@ -397,65 +310,35 @@
     }
 
     /**
-     * @return true if should wait for the valid windows state.
-     */
-    private fun shouldWaitForWindows(state: Dump): Boolean {
-        return when {
-            state.wmState.frontWindow == null -> {
-                Log.i(LOG_TAG, "***frontWindow=null")
-                true
-            }
-            state.wmState.focusedWindow.isEmpty() -> {
-                Log.i(LOG_TAG, "***focusedWindow=null")
-                true
-            }
-            state.wmState.focusedApp.isEmpty() -> {
-                Log.i(LOG_TAG, "***focusedApp=null")
-                true
-            }
-            else -> false
-        }
-    }
-
-    private fun shouldWaitForValidityCheck(state: Dump): Boolean {
-        return !state.wmState.isComplete()
-    }
-
-    /**
      * Waits until the IME window and layer are visible
      */
     @JvmOverloads
-    fun waitImeWindowShown(displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
-        waitFor("IME window shown") {
-            val imeSurfaceShown = it.wmState.inputMethodWindowState?.isSurfaceShown == true
-            val imeDisplay = it.wmState.inputMethodWindowState?.displayId
-            val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME)
-            val result = imeSurfaceShown && imeLayerShown && imeDisplay == displayId
-
-            Log.v(LOG_TAG, "Current: $result " +
-                "imeSurfaceShown($imeSurfaceShown) " +
-                "imeLayerShown($imeLayerShown) " +
-                "imeDisplay($imeDisplay)")
-            result
-        }
+    fun waitImeShown(displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isImeShown(displayId))
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
 
     /**
      * Waits until the IME layer is no longer visible. Cannot wait for the window as
      * its visibility information is updated at a later state and is not reliable in
      * the trace
      */
-    fun waitImeWindowGone(): Boolean =
-        waitFor("IME window gone") {
-            val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME)
-            Log.v(LOG_TAG, "imeLayerShown($imeLayerShown)")
-            !imeLayerShown
-        }
+    fun waitImeGone(): Boolean =
+        createConditionBuilder()
+            .withCondition(WindowManagerConditionsFactory.isLayerVisible(IME).negate())
+            .withCondition(
+                WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
+            .build()
+            .waitFor()
 
     /**
      * Obtains a [WindowContainer] from the current device state, or null if the WindowContainer
      * doesn't exist
      */
-    fun getWindow(activity: ComponentName): WindowState? {
+    fun getWindow(activity: FlickerComponentName): WindowState? {
         val windowName = activity.toWindowName()
         return this.currentState.wmState.windowStates
             .firstOrNull { it.title == windowName }
@@ -464,40 +347,8 @@
     /**
      * Obtains the region of a window in the state, or an empty [Rect] is there are none
      */
-    fun getWindowRegion(activity: ComponentName): Region {
+    fun getWindowRegion(activity: FlickerComponentName): Region {
         val window = getWindow(activity)
         return window?.frameRegion?.toAndroidRegion() ?: Region()
     }
-
-    companion object {
-        @VisibleForTesting
-        const val NAV_BAR_WINDOW_NAME = "NavigationBar0"
-        @VisibleForTesting
-        const val STATUS_BAR_WINDOW_NAME = "StatusBar"
-        @VisibleForTesting
-        const val NAV_BAR_LAYER_NAME = "$NAV_BAR_WINDOW_NAME#0"
-        @VisibleForTesting
-        const val STATUS_BAR_LAYER_NAME = "$STATUS_BAR_WINDOW_NAME#0"
-        @VisibleForTesting
-        const val ROTATION_LAYER_NAME = "RotationLayer#0"
-        @VisibleForTesting
-        const val BLACK_SURFACE_LAYER_NAME = "BackColorSurface#0"
-        @VisibleForTesting
-        const val IME_LAYER_NAME = "InputMethod#0"
-        @VisibleForTesting
-        const val SPLASH_SCREEN_NAME = "Splash Screen"
-        @VisibleForTesting
-        const val SNAPSHOT_WINDOW_NAME = "SnapshotStartingWindow"
-    }
-
-    data class Dump(
-        /**
-         * Window manager state
-         */
-        @JvmField val wmState: WindowManagerState,
-        /**
-         * Layers state
-         */
-        @JvmField val layerState: LayerTraceEntry
-    )
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt
index 718b0b3..010f421 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt
@@ -22,42 +22,45 @@
 import android.util.Log
 import android.view.nano.ViewProtoEnums
 import android.view.nano.WindowLayoutParamsProto
-import com.android.server.wm.traces.common.windowmanager.windows.Configuration
-import com.android.server.wm.traces.common.Rect
-import com.android.server.wm.traces.common.windowmanager.windows.WindowConfiguration
-import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
-import com.android.server.wm.traces.common.windowmanager.WindowManagerState
-import com.android.server.wm.traces.common.windowmanager.windows.Activity
-import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask
-import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer
-import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea
-import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent
-import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState
-import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer
-import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
-import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy
-import com.android.server.wm.traces.common.windowmanager.windows.WindowState
-import com.android.server.wm.traces.common.windowmanager.windows.WindowToken
 import com.android.server.wm.nano.ActivityRecordProto
 import com.android.server.wm.nano.AppTransitionProto
 import com.android.server.wm.nano.ConfigurationContainerProto
 import com.android.server.wm.nano.DisplayAreaProto
 import com.android.server.wm.nano.DisplayContentProto
 import com.android.server.wm.nano.KeyguardControllerProto
-import com.android.server.wm.nano.WindowManagerPolicyProto
 import com.android.server.wm.nano.RootWindowContainerProto
+import com.android.server.wm.nano.TaskFragmentProto
 import com.android.server.wm.nano.TaskProto
 import com.android.server.wm.nano.WindowContainerChildProto
 import com.android.server.wm.nano.WindowContainerProto
+import com.android.server.wm.nano.WindowManagerPolicyProto
 import com.android.server.wm.nano.WindowManagerServiceDumpProto
 import com.android.server.wm.nano.WindowManagerTraceFileProto
 import com.android.server.wm.nano.WindowStateProto
 import com.android.server.wm.nano.WindowTokenProto
-import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.Size
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.common.windowmanager.windows.Activity
+import com.android.server.wm.traces.common.windowmanager.windows.Configuration
+import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent
+import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState
+import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.Task
+import com.android.server.wm.traces.common.windowmanager.windows.TaskFragment
+import com.android.server.wm.traces.common.windowmanager.windows.WindowConfiguration
+import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
 import com.android.server.wm.traces.common.windowmanager.windows.WindowLayoutParams
+import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.android.server.wm.traces.common.windowmanager.windows.WindowToken
 import com.android.server.wm.traces.parser.LOG_TAG
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException
 import java.nio.file.Path
+import kotlin.math.max
 import kotlin.system.measureTimeMillis
 
 object WindowManagerTraceParser {
@@ -83,16 +86,14 @@
      *
      * @param data binary proto data
      * @param source Path to source of data for additional debug information
-     * @param checksum File SHA512 checksum
      */
     @JvmOverloads
     @JvmStatic
     fun parseFromTrace(
         data: ByteArray?,
-        source: Path? = null,
-        checksum: String = ""
+        source: Path? = null
     ): WindowManagerTrace {
-        val fileProto: WindowManagerTraceFileProto
+        var fileProto: WindowManagerTraceFileProto? = null
         try {
             measureTimeMillis {
                 fileProto = WindowManagerTraceFileProto.parseFrom(data)
@@ -102,7 +103,9 @@
         } catch (e: InvalidProtocolBufferNanoException) {
             throw RuntimeException(e)
         }
-        return parseFromTrace(fileProto, source, checksum)
+
+        return fileProto?.let { parseFromTrace(it, source) }
+                ?: error("Unable to read trace file")
     }
 
     /**
@@ -110,14 +113,12 @@
      *
      * @param proto Parsed proto data
      * @param source Path to source of data for additional debug information
-     * @param checksum File SHA512 checksum
      */
     @JvmOverloads
     @JvmStatic
     fun parseFromTrace(
         proto: WindowManagerTraceFileProto,
-        source: Path? = null,
-        checksum: String = ""
+        source: Path? = null
     ): WindowManagerTrace {
         val entries = mutableListOf<WindowManagerState>()
         var traceParseTime = 0L
@@ -131,9 +132,8 @@
         }
 
         Log.v(LOG_TAG, "Parsing duration (WM Trace): ${traceParseTime}ms " +
-            "(avg ${traceParseTime / entries.size}ms per entry)")
-        return WindowManagerTrace(entries, source?.toAbsolutePath()?.toString()
-            ?: "", checksum)
+            "(avg ${traceParseTime / max(entries.size, 1)}ms per entry)")
+        return WindowManagerTrace(entries.toTypedArray(), "${source?.toAbsolutePath()}")
     }
 
     /**
@@ -145,9 +145,8 @@
     @JvmStatic
     fun parseFromDump(proto: WindowManagerServiceDumpProto): WindowManagerTrace {
         return WindowManagerTrace(
-            listOf(newTraceEntry(proto, timestamp = 0, where = "")),
-            source = "",
-            sourceChecksum = "")
+                arrayOf(newTraceEntry(proto, timestamp = 0, where = "")),
+            source = "")
     }
 
     /**
@@ -163,7 +162,7 @@
         } catch (e: InvalidProtocolBufferNanoException) {
             throw RuntimeException(e)
         }
-        return WindowManagerTrace(parseFromDump(fileProto), source = "", sourceChecksum = "")
+        return parseFromDump(fileProto)
     }
 
     private fun newTraceEntry(
@@ -189,11 +188,11 @@
             root = newRootWindowContainer(proto.rootWindowContainer),
             keyguardControllerState = newKeyguardControllerState(
                 proto.rootWindowContainer.keyguardController),
-            timestamp = timestamp
+            _timestamp = timestamp.toString()
         )
     }
 
-    private fun newWindowManagerPolicy(proto: WindowManagerPolicyProto): WindowManagerPolicy? {
+    private fun newWindowManagerPolicy(proto: WindowManagerPolicyProto): WindowManagerPolicy {
         return WindowManagerPolicy(
             focusedAppToken = proto.focusedAppToken ?: "",
             forceStatusBar = proto.forceStatusBar,
@@ -240,7 +239,9 @@
     ): WindowContainer? {
         return newDisplayContent(proto.displayContent, isActivityInTree)
             ?: newDisplayArea(proto.displayArea, isActivityInTree)
-            ?: newTask(proto.task, isActivityInTree) ?: newActivity(proto.activity)
+            ?: newTask(proto.task, isActivityInTree)
+            ?: newTaskFragment(proto.taskFragment, isActivityInTree)
+            ?: newActivity(proto.activity)
             ?: newWindowToken(proto.windowToken, isActivityInTree)
             ?: newWindowState(proto.window, isActivityInTree)
             ?: newWindowContainer(proto.windowContainer, children = emptyList())
@@ -258,8 +259,10 @@
                 focusedRootTaskId = proto.focusedRootTaskId,
                 resumedActivity = proto.resumedActivity?.title ?: "",
                 singleTaskInstance = proto.singleTaskInstance,
-                _defaultPinnedStackBounds = proto.pinnedTaskController?.defaultBounds?.toRect(),
-                _pinnedStackMovementBounds = proto.pinnedTaskController?.movementBounds?.toRect(),
+                defaultPinnedStackBounds = proto.pinnedTaskController?.defaultBounds?.toRect()
+                    ?: Rect.EMPTY,
+                pinnedStackMovementBounds = proto.pinnedTaskController?.movementBounds?.toRect()
+                    ?: Rect.EMPTY,
                 displayRect = Rect(0, 0, proto.displayInfo?.logicalWidth
                     ?: 0, proto.displayInfo?.logicalHeight ?: 0),
                 appRect = Rect(0, 0, proto.displayInfo?.appWidth ?: 0,
@@ -267,7 +270,7 @@
                         ?: 0),
                 dpi = proto.dpi,
                 flags = proto.displayInfo?.flags ?: 0,
-                _stableBounds = proto.displayFrames?.stableBounds?.toRect(),
+                stableBounds = proto.displayFrames?.stableBounds?.toRect() ?: Rect.EMPTY,
                 surfaceSize = proto.surfaceSize,
                 focusedApp = proto.focusedApp,
                 lastTransition = appTransitionToString(
@@ -301,18 +304,18 @@
         }
     }
 
-    private fun newTask(proto: TaskProto?, isActivityInTree: Boolean): ActivityTask? {
+    private fun newTask(proto: TaskProto?, isActivityInTree: Boolean): Task? {
         return if (proto == null) {
             null
         } else {
-            ActivityTask(
-                activityType = proto.activityType,
+            Task(
+                activityType = proto.taskFragment?.activityType ?: proto.activityType,
                 isFullscreen = proto.fillsParent,
                 bounds = proto.bounds.toRect(),
                 taskId = proto.id,
                 rootTaskId = proto.rootTaskId,
-                displayId = proto.displayId,
-                _lastNonFullscreenBounds = proto.lastNonFullscreenBounds?.toRect(),
+                displayId = proto.taskFragment?.displayId ?: proto.displayId,
+                lastNonFullscreenBounds = proto.lastNonFullscreenBounds?.toRect() ?: Rect.EMPTY,
                 realActivity = proto.realActivity,
                 origActivity = proto.origActivity,
                 resizeMode = proto.resizeMode,
@@ -321,6 +324,30 @@
                 surfaceWidth = proto.surfaceWidth,
                 surfaceHeight = proto.surfaceHeight,
                 createdByOrganizer = proto.createdByOrganizer,
+                minWidth = proto.taskFragment?.minWidth ?: proto.minWidth,
+                minHeight = proto.taskFragment?.minHeight ?: proto.minHeight,
+                windowContainer = newWindowContainer(
+                    proto.taskFragment?.windowContainer ?: proto.windowContainer,
+                    if (proto.taskFragment != null) {
+                        proto.taskFragment.windowContainer.children
+                                .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) }
+                    } else {
+                        proto.windowContainer.children
+                                .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) }
+                    }
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newTaskFragment(proto: TaskFragmentProto?, isActivityInTree: Boolean):
+            TaskFragment? {
+        return if (proto == null) {
+            null
+        } else {
+            TaskFragment(
+                activityType = proto.activityType,
+                displayId = proto.displayId,
                 minWidth = proto.minWidth,
                 minHeight = proto.minHeight,
                 windowContainer = newWindowContainer(
@@ -385,16 +412,16 @@
                         WindowState.WINDOW_TYPE_STARTING
                     else -> 0
                 },
-                requestedSize = Bounds(proto.requestedWidth, proto.requestedHeight),
+                requestedSize = Size(proto.requestedWidth, proto.requestedHeight),
                 surfacePosition = proto.surfacePosition?.toRect(),
-                _frame = proto.windowFrames?.frame?.toRect(),
-                _containingFrame = proto.windowFrames?.containingFrame?.toRect(),
-                _parentFrame = proto.windowFrames?.parentFrame?.toRect(),
-                _contentFrame = proto.windowFrames?.contentFrame?.toRect(),
-                _contentInsets = proto.windowFrames?.contentInsets?.toRect(),
-                _surfaceInsets = proto.surfaceInsets?.toRect(),
-                _givenContentInsets = proto.givenContentInsets?.toRect(),
-                _crop = proto.animator?.lastClipRect?.toRect(),
+                frame = proto.windowFrames?.frame?.toRect() ?: Rect.EMPTY,
+                containingFrame = proto.windowFrames?.containingFrame?.toRect() ?: Rect.EMPTY,
+                parentFrame = proto.windowFrames?.parentFrame?.toRect() ?: Rect.EMPTY,
+                contentFrame = proto.windowFrames?.contentFrame?.toRect() ?: Rect.EMPTY,
+                contentInsets = proto.windowFrames?.contentInsets?.toRect() ?: Rect.EMPTY,
+                surfaceInsets = proto.surfaceInsets?.toRect() ?: Rect.EMPTY,
+                givenContentInsets = proto.givenContentInsets?.toRect() ?: Rect.EMPTY,
+                crop = proto.animator?.lastClipRect?.toRect() ?: Rect.EMPTY,
                 windowContainer = newWindowContainer(
                     proto.windowContainer,
                     proto.windowContainer.children
@@ -504,7 +531,8 @@
                 _isVisible = proto.visible,
                 configurationContainer = newConfigurationContainer(
                     proto.configurationContainer),
-                children.toTypedArray()
+                layerId = proto.surfaceControl?.layerId ?: 0,
+                children = children.toTypedArray()
             )
         }
     }
@@ -551,4 +579,4 @@
     }
 
     private fun RectProto.toRect() = Rect(this.left, this.top, this.right, this.bottom)
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/test/Android.bp b/libraries/flicker/test/Android.bp
index 1af44fe..314f4aa 100644
--- a/libraries/flicker/test/Android.bp
+++ b/libraries/flicker/test/Android.bp
@@ -29,6 +29,9 @@
     test_suites: ["device-tests"],
     srcs: ["src/**/*.kt"],
     libs: ["android.test.runner"],
+    optimize: {
+        enabled: false
+    },
     static_libs: [
         "flickerlib",
         "launcher-aosp-tapl"
diff --git a/libraries/flicker/test/AndroidManifest.xml b/libraries/flicker/test/AndroidManifest.xml
index 20b8f0a..27e1dad 100644
--- a/libraries/flicker/test/AndroidManifest.xml
+++ b/libraries/flicker/test/AndroidManifest.xml
@@ -20,6 +20,10 @@
     <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
     <!-- Enable / Disable tracing !-->
     <uses-permission android:name="android.permission.DUMP" />
+    <!-- ATM.removeRootTasksWithActivityTypes() -->
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <!-- Force-stop test apps -->
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
     <!-- Allow the test to write directly to /sdcard/ -->
     <application android:label="FlickerLibTest"
                  android:requestLegacyExternalStorage="true">
diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..3648f04
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope
new file mode 100644
index 0000000..2c3cb1d
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope
new file mode 100644
index 0000000..a6d57f3
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope
new file mode 100644
index 0000000..d45d04a
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..92d3d76
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope
new file mode 100644
index 0000000..fe3a6ac
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope
new file mode 100644
index 0000000..91538fb
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json b/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json
new file mode 100644
index 0000000..2350b75
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json
@@ -0,0 +1,120 @@
+{
+  "assertors": [
+    {
+      "transition": "ROTATION",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways"
+          }
+        ]
+      }
+    },
+    {
+      "transition": "APP_LAUNCH",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways"
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/assets/testdata/assertors/config.json b/libraries/flicker/test/assets/testdata/assertors/config.json
new file mode 100644
index 0000000..174da8e
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/config.json
@@ -0,0 +1,127 @@
+{
+  "assertors": [
+    {
+      "transition": "ROTATION",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": []
+      }
+    },
+    {
+      "transition": "APP_LAUNCH",
+      "assertions": {
+        "presubmit": [
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/NavigationBar0"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways",
+            "args": [
+              "/StatusBar"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerReplacesLauncher"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart",
+            "args": [
+              "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAtEnd",
+            "args": [
+              "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity"
+            ]
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsInvisibleAtStart"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsVisibleAtEnd"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.AppWindowReplacesLauncherAsTopWindow"
+          },
+          {
+            "class": "com.android.server.wm.flicker.service.assertors.common.LauncherWindowMovesOutOfTop"
+          }
+        ],
+        "postsubmit": [],
+        "flaky": []
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope
new file mode 100644
index 0000000..645f8be
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..4bdb322
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope
new file mode 100644
index 0000000..f146dc2
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb b/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb
new file mode 100644
index 0000000..430b3cc
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope b/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope
new file mode 100644
index 0000000..59964cc
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb b/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb
new file mode 100644
index 0000000..0c1693c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb b/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb
new file mode 100644
index 0000000..ba58762
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb b/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb
new file mode 100644
index 0000000..71e3f47
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..b20d68c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope
new file mode 100644
index 0000000..b83c2ab
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..e62a755
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope
new file mode 100644
index 0000000..26c1c8c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..1a43a03
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope
new file mode 100644
index 0000000..2cb8149
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..08d2743
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope
new file mode 100644
index 0000000..96d30a0
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..1a1de61
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope
new file mode 100644
index 0000000..8fb9200
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..5871543
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope
new file mode 100644
index 0000000..3ff92c6
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..ac15b8a
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope
new file mode 100644
index 0000000..7503f80
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..935c7d0
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope
new file mode 100644
index 0000000..28d7755
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..6bccfc1
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope
new file mode 100644
index 0000000..16171b3
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..00f2be7
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope
new file mode 100644
index 0000000..31a914c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..8322e0b
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope
new file mode 100644
index 0000000..3a38466
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..aa5cb0b
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope
new file mode 100644
index 0000000..d25769b
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..123dae7
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope
new file mode 100644
index 0000000..22d7826
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..130dad9
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope
new file mode 100644
index 0000000..6f7639a
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..a09d715
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope
new file mode 100644
index 0000000..4059964
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..3ba3f02
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope
new file mode 100644
index 0000000..691fff8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..9f01cd2
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope
new file mode 100644
index 0000000..8c75f7c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..27ab281
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope
new file mode 100644
index 0000000..0610672
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..6c919c7
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope
new file mode 100644
index 0000000..2cf4606
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..6011ac8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope
new file mode 100644
index 0000000..3e97ade
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..31e22dd
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope
new file mode 100644
index 0000000..296cd45
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..8859bb8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope
new file mode 100644
index 0000000..e5b1e11
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..7b7c571
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope
new file mode 100644
index 0000000..3534808
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..c833fb4
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope
new file mode 100644
index 0000000..633acee
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..c833fb4
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope
new file mode 100644
index 0000000..633acee
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..351dd17
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope
new file mode 100644
index 0000000..55a179c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..9d3bb50
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope
new file mode 100644
index 0000000..def491f
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..e898878
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope
new file mode 100644
index 0000000..ead45ab
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..36fc663
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope
new file mode 100644
index 0000000..b5537ac
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..388f4e8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope
new file mode 100644
index 0000000..e7c8c4c
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..bcca47e
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope
new file mode 100644
index 0000000..6c276fb
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb b/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb
new file mode 100755
index 0000000..82a84c4
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb b/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb
new file mode 100755
index 0000000..30eb3e6
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb b/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb
new file mode 100644
index 0000000..893bda9
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb
Binary files differ
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt
index f82d6df..b398ae5 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt
@@ -21,6 +21,7 @@
 import com.android.server.wm.flicker.traces.FlickerFailureStrategy
 import com.android.server.wm.flicker.traces.FlickerSubjectException
 import com.android.server.wm.traces.common.ITraceEntry
+import com.google.common.truth.Fact
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.StandardSubjectBuilder
 import com.google.common.truth.Subject
@@ -50,6 +51,47 @@
     }
 
     @Test
+    fun canCheckChangingAssertions_IgnoreOptionalStart() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun canCheckChangingAssertions_IgnoreOptionalEnd() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun canCheckChangingAssertions_IgnoreOptionalMiddle() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData0") { it.isData0() }
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun canCheckChangingAssertions_IgnoreOptionalMultiple() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData0") { it.isData0() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.add("isData1", isOptional = true) { it.isData1() }
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
     fun canCheckChangingAssertions_withNoAssertions() {
         val checker = AssertionsChecker<SimpleEntrySubject>()
         checker.test(getTestEntries(42, 0, 0, 0, 0))
@@ -98,7 +140,7 @@
             require(failure is FlickerSubjectException) { "Unknown failure $failure" }
             assertFailure(failure.cause)
                 .hasMessageThat()
-                .contains("Assertion never became false: isData42")
+                .contains("Assertion never failed: isData42")
         }
     }
 
@@ -121,7 +163,9 @@
         failureMetadata: FailureMetadata,
         private val entry: SimpleEntry
     ) : FlickerSubject(failureMetadata, entry) {
-        override val defaultFacts: String = "SimpleEntry(${entry.mData})"
+        override val timestamp: Long get() = 0
+        override val parent: FlickerSubject? get() = null
+        override val selfFacts = listOf(Fact.fact("SimpleEntry", entry.mData.toString()))
         override fun clone(): FlickerSubject {
             return SimpleEntrySubject(fm, entry)
         }
@@ -134,6 +178,10 @@
             check("is0").that(entry.mData).isEqualTo(0)
         }
 
+        fun isData1() = apply {
+            check("is1").that(entry.mData).isEqualTo(1)
+        }
+
         companion object {
             /**
              * Boiler-plate Subject.Factory for LayersTraceSubject
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt
new file mode 100644
index 0000000..07bd72f
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:JvmName("CommonConstants")
+package com.android.server.wm.flicker
+
+import com.android.server.wm.traces.common.FlickerComponentName
+
+val CHROME_COMPONENT = FlickerComponentName("com.android.chrome",
+        "org.chromium.chrome.browser.firstrun.FirstRunActivity")
+val CHROME_SPLASH_SCREEN_COMPONENT = FlickerComponentName("", "Splash Screen com.android.chrome")
+val DOCKER_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider")
+val IMAGINARY_COMPONENT = FlickerComponentName("", "ImaginaryWindow")
+val IME_ACTIVITY_COMPONENT = FlickerComponentName("com.android.server.wm.flicker.testapp",
+        "com.android.server.wm.flicker.testapp.ImeActivity")
+val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher",
+        "com.google.android.apps.nexuslauncher.NexusLauncherActivity")
+val PIP_DISMISS_COMPONENT = FlickerComponentName("", "pip-dismiss-overlay")
+
+val SIMPLE_APP_COMPONENT = FlickerComponentName("com.android.server.wm.flicker.testapp",
+        "com.android.server.wm.flicker.testapp.SimpleActivity")
+private const val SHELL_PKG_NAME = "com.android.wm.shell.flicker.testapp"
+val SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT = FlickerComponentName(SHELL_PKG_NAME,
+        "$SHELL_PKG_NAME.SplitScreenActivity")
+val SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT = FlickerComponentName(SHELL_PKG_NAME,
+        "$SHELL_PKG_NAME.SplitScreenSecondaryActivity")
+
+val SCREEN_DECOR_COMPONENT = FlickerComponentName("", "ScreenDecorOverlay")
+val WALLPAPER_COMPONENT = FlickerComponentName(
+        "", "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2")
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt
index a894632..e8891f4 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt
@@ -34,14 +34,14 @@
                         FocusEvent(0, "WinB", FocusEvent.Focus.LOST, "test"),
                         FocusEvent(0, "test WinC", FocusEvent.Focus.GAINED, "test"))
         val result = builder.buildEventLogResult().eventLogSubject
-        require(result != null) { "Event log subject was not built" }
-        result.focusChanges(arrayOf("WinA", "WinB", "WinC"))
+        requireNotNull(result) { "Event log subject was not built" }
+        result.focusChanges("WinA", "WinB", "WinC")
                 .forAllEntries()
-        result.focusChanges(arrayOf("WinA", "WinB")).forAllEntries()
-        result.focusChanges(arrayOf("WinB", "WinC")).forAllEntries()
-        result.focusChanges(arrayOf("WinA")).forAllEntries()
-        result.focusChanges(arrayOf("WinB")).forAllEntries()
-        result.focusChanges(arrayOf("WinC")).forAllEntries()
+        result.focusChanges("WinA", "WinB").forAllEntries()
+        result.focusChanges("WinB", "WinC").forAllEntries()
+        result.focusChanges("WinA").forAllEntries()
+        result.focusChanges("WinB").forAllEntries()
+        result.focusChanges("WinC").forAllEntries()
     }
 
     @Test
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
index dd8d15e..ba4bb62 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
@@ -228,4 +228,31 @@
                 .contains(exceptionMessage)
         }
     }
+
+    private val failedAssertion = AssertionData(tag = AssertionTag.END,
+            expectedSubjectClass = LayerTraceEntrySubject::class) {
+        this.fail("Expected exception")
+    }
+
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val builder = FlickerBuilder(instrumentation)
+        builder.transitions { device.pressHome() }
+        val flicker = builder.build()
+        flicker.execute()
+
+        val error = assertThrows(AssertionError::class.java) {
+            flicker.checkAssertion(failedAssertion)
+        }
+        // Exception message
+        Truth.assertThat(error).hasMessageThat().contains("Expected exception")
+        // Subject facts
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace files")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Location")
+        // Correct stack trace point
+        Truth.assertThat(error).hasMessageThat().contains("failedAssertion")
+    }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt
deleted file mode 100644
index cb99b65..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker
-
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
-import com.android.server.wm.traces.common.Bounds
-import com.google.common.truth.Truth
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runners.MethodSorters
-
-/**
- * Contains [LayerSubject] tests. To run this test:
- * `atest FlickerLibTest:LayerSubjectTest`
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LayerSubjectTest {
-    @Test
-    fun exceptionContainsDebugInfo() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
-        val error = assertThrows(AssertionError::class.java) {
-            assertThat(layersTraceEntries)
-                .first()
-                .layer("ImaginaryLayer", 0)
-                .exists()
-        }
-        Truth.assertThat(error).hasMessageThat().contains("Trace:")
-        Truth.assertThat(error).hasMessageThat().contains("Path: ")
-        Truth.assertThat(error).hasMessageThat().contains("Entry:")
-        Truth.assertThat(error).hasMessageThat().contains("Frame:")
-        Truth.assertThat(error).hasMessageThat().contains("Layer:")
-    }
-
-    @Test
-    fun canTestAssertionsOnLayer() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
-        assertThat(layersTraceEntries)
-            .layer("SoundVizWallpaperV2", 26033)
-            .hasBufferSize(Bounds(1440, 2960))
-            .hasScalingMode(0)
-
-        assertThat(layersTraceEntries)
-            .layer("DoesntExist", 1)
-            .doesNotExist()
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt
deleted file mode 100644
index d47820e..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker
-
-import android.graphics.Region
-import androidx.test.filters.FlakyTest
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
-import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
-import com.android.server.wm.traces.common.layers.LayersTrace
-import com.google.common.truth.Truth
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runners.MethodSorters
-
-/**
- * Contains [LayersTraceSubject] tests. To run this test: `atest
- * FlickerLibTest:LayersTraceSubjectTest`
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LayersTraceSubjectTest {
-    @Test
-    fun exceptionContainsDebugInfo() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
-        val error = assertThrows(AssertionError::class.java) {
-            assertThat(layersTraceEntries)
-                .isEmpty()
-        }
-        Truth.assertThat(error).hasMessageThat().contains("Trace:")
-        Truth.assertThat(error).hasMessageThat().contains("Path: ")
-        Truth.assertThat(error).hasMessageThat().contains("Start:")
-        Truth.assertThat(error).hasMessageThat().contains("End:")
-    }
-
-    @Test
-    fun testCanDetectEmptyRegionFromLayerTrace() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
-        try {
-            assertThat(layersTraceEntries)
-                .coversAtLeast(DISPLAY_REGION)
-                .forAllEntries()
-            error("Assertion should not have passed")
-        } catch (e: Throwable) {
-            assertFailure(e).factValue("Region to test").contains(DISPLAY_REGION.toString())
-            assertFailure(e).factValue("Uncovered region").contains("SkRegion((0,1440,1440,2880))")
-        }
-    }
-
-    @Test
-    fun testCanInspectBeginning() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
-        assertThat(layersTraceEntries)
-            .first()
-            .isVisible("NavigationBar0#0")
-            .notContains("DockedStackDivider#0")
-            .isVisible("NexusLauncherActivity#0")
-    }
-
-    @Test
-    fun testCanInspectEnd() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
-        assertThat(layersTraceEntries)
-            .last()
-            .isVisible("NavigationBar0#0")
-            .isVisible("DockedStackDivider#0")
-    }
-
-    @Test
-    fun testCanDetectChangingAssertions() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
-        assertThat(layersTraceEntries)
-            .isVisible("NavigationBar0#0")
-            .notContains("DockedStackDivider#0")
-            .then()
-            .isVisible("NavigationBar0#0")
-            .isInvisible("DockedStackDivider#0")
-            .then()
-            .isVisible("NavigationBar0#0")
-            .isVisible("DockedStackDivider#0")
-            .forAllEntries()
-    }
-
-    @FlakyTest
-    @Test
-    fun testCanDetectIncorrectVisibilityFromLayerTrace() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb")
-        val error = assertThrows(AssertionError::class.java) {
-            assertThat(layersTraceEntries)
-                .isVisible("com.android.server.wm.flicker.testapp")
-                .then()
-                .isInvisible("com.android.server.wm.flicker.testapp")
-                .forAllEntries()
-        }
-
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("layers_trace_invalid_layer_visibility.pb")
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("2d22h13m14s303ms")
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("!isVisible")
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("com.android.server.wm.flicker.testapp/" +
-                "com.android.server.wm.flicker.testapp.SimpleActivity#0 is visible")
-    }
-
-    @Test
-    fun testCanDetectInvalidVisibleLayerForMoreThanOneConsecutiveEntry() {
-        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_visible_layers.pb")
-        val error = assertThrows(AssertionError::class.java) {
-            assertThat(layersTraceEntries)
-                .visibleLayersShownMoreThanOneConsecutiveEntry()
-                .forAllEntries()
-            error("Assertion should not have passed")
-        }
-
-        Truth.assertThat(error).hasMessageThat().contains("2d18h35m56s397ms")
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("StatusBar#0")
-        assertFailure(error)
-            .hasMessageThat()
-            .contains("is not visible for 2 entries")
-    }
-
-    private fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(trace: LayersTrace) {
-        assertThat(trace)
-            .visibleLayersShownMoreThanOneConsecutiveEntry()
-            .forAllEntries()
-    }
-
-    @Test
-    fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry() {
-        testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(
-            readLayerTraceFromFile("layers_trace_snapshot_visible.pb"))
-    }
-
-    @Test
-    fun testCanIgnoreLayerEqualNameInVisibleLayersMoreThanOneConsecutiveEntry() {
-        val layersTraceEntries = readLayerTraceFromFile(
-                "layers_trace_invalid_visible_layers.pb")
-        assertThat(layersTraceEntries)
-                .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("StatusBar#0"))
-                .forAllEntries()
-    }
-
-    @Test
-    fun testCanIgnoreLayerShorterNameInVisibleLayersMoreThanOneConsecutiveEntry() {
-        val layersTraceEntries = readLayerTraceFromFile(
-                "one_visible_layer_launcher_trace.pb")
-        assertThat(layersTraceEntries)
-                .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("Launcher"))
-                .forAllEntries()
-    }
-
-    private fun detectRootLayer(fileName: String) {
-        val layersTrace = readLayerTraceFromFile(fileName)
-        for (entry in layersTrace.entries) {
-            val rootLayers = entry.rootLayers
-            Truth.assertWithMessage("Does not have any root layer")
-                    .that(rootLayers.size)
-                    .isGreaterThan(0)
-            val firstParentId = rootLayers.first().parentId
-            Truth.assertWithMessage("Has multiple root layers")
-                    .that(rootLayers.all { it.parentId == firstParentId })
-                    .isTrue()
-        }
-    }
-
-    @Test
-    fun testCanDetectRootLayer() {
-        detectRootLayer("layers_trace_root.pb")
-    }
-
-    @Test
-    fun testCanDetectRootLayerAOSP() {
-        detectRootLayer("layers_trace_root_aosp.pb")
-    }
-
-    companion object {
-        private val DISPLAY_REGION = Region(0, 0, 1440, 2880)
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt
deleted file mode 100644
index 3016a0a..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker
-
-import com.android.server.wm.traces.common.layers.LayersTrace
-import com.google.common.truth.Truth
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runners.MethodSorters
-
-/**
- * Contains [LayersTrace] tests. To run this test: `atest
- * FlickerLibTest:LayersTraceTest`
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LayersTraceTest {
-    private fun detectRootLayer(fileName: String) {
-        val layersTrace = readLayerTraceFromFile(fileName)
-        for (entry in layersTrace.entries) {
-            val rootLayers = entry.rootLayers
-            Truth.assertWithMessage("Does not have any root layer")
-                .that(rootLayers.size)
-                .isGreaterThan(0)
-            val firstParentId = rootLayers.first().parentId
-            Truth.assertWithMessage("Has multiple root layers")
-                .that(rootLayers.all { it.parentId == firstParentId })
-                .isTrue()
-        }
-    }
-
-    @Test
-    fun testCanDetectRootLayer() {
-        detectRootLayer("layers_trace_root.pb")
-    }
-
-    @Test
-    fun testCanDetectRootLayerAOSP() {
-        detectRootLayer("layers_trace_root_aosp.pb")
-    }
-
-    @Test
-    fun testCanParseTraceWithoutHWC() {
-        val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb")
-        layersTrace.forEach { entry ->
-            Truth.assertWithMessage("Should have visible layers in all trace entries")
-                .that(entry.visibleLayers).asList()
-                .isNotEmpty()
-        }
-    }
-}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt
index 8814113..9161a5d 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt
@@ -17,7 +17,9 @@
 package com.android.server.wm.flicker
 
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_LAYERS
 import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_WM
 import com.android.server.wm.traces.parser.WmStateDumpFlags
@@ -29,7 +31,7 @@
 import org.junit.runners.MethodSorters
 
 /**
- * Contains [UiDeviceExtensions] tests.
+ * Contains [com.android.server.wm.traces.parser.Extensions] tests.
  *
  * To run this test: `atest FlickerLibTest:UiDeviceExtensionsTest`
  */
@@ -44,7 +46,7 @@
 
     private fun getCurrStateDump(
         @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
-    ): DeviceStateDump {
+    ): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> {
         val instrumentation = InstrumentationRegistry.getInstrumentation()
         return getCurrentStateDump(instrumentation.uiAutomation, dumpFlags)
     }
@@ -62,8 +64,8 @@
         Truth.assertThat(currStateDump.first).isNotEmpty()
         Truth.assertThat(currStateDump.second).isEmpty()
         val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_WM)
-        Truth.assertThat(currState.wmTrace).isNotNull()
-        Truth.assertThat(currState.layersTrace).isNull()
+        Truth.assertThat(currState.wmState).isNotNull()
+        Truth.assertThat(currState.layerState).isNull()
     }
 
     @Test
@@ -72,16 +74,22 @@
         Truth.assertThat(currStateDump.first).isEmpty()
         Truth.assertThat(currStateDump.second).isNotEmpty()
         val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_LAYERS)
-        Truth.assertThat(currState.wmTrace).isNull()
-        Truth.assertThat(currState.layersTrace).isNotNull()
+        Truth.assertThat(currState.wmState).isNull()
+        Truth.assertThat(currState.layerState).isNotNull()
     }
 
     @Test
     fun canParseCurrentDeviceState() {
         val currState = this.getCurrStateDump()
-        Truth.assertThat(currState.wmTrace?.entries).hasSize(1)
-        Truth.assertThat(currState.wmTrace?.entries?.first()?.windowStates).isNotEmpty()
-        Truth.assertThat(currState.layersTrace?.entries).hasSize(1)
-        Truth.assertThat(currState.layersTrace?.entries?.first()?.flattenedLayers).isNotEmpty()
+        Truth.assertThat(currState.wmState?.asTrace()?.entries ?: emptyArray())
+            .asList()
+            .hasSize(1)
+        Truth.assertThat(currState.wmState?.asTrace()?.entries?.first()?.windowStates)
+            .isNotEmpty()
+        Truth.assertThat(currState.layerState?.asTrace()?.entries ?: emptyArray())
+            .asList()
+            .hasSize(1)
+        Truth.assertThat(currState.layerState?.asTrace()?.entries?.first()?.flattenedLayers)
+            .isNotEmpty()
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt
index bc376b2..4d2869f 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt
@@ -20,8 +20,10 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.traces.FlickerSubjectException
 import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.tags.TagTrace
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.tags.TagTraceParserUtil
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
 import com.google.common.io.ByteStreams
 import com.google.common.truth.ExpectFailure
@@ -57,6 +59,15 @@
     }
 }
 
+internal fun readTagTraceFromFile(relativePath: String): TagTrace {
+    return try {
+        TagTraceParserUtil.parseFromTrace(readTestFile(relativePath),
+            source = Paths.get(relativePath))
+    } catch (e: Exception) {
+        throw RuntimeException(e)
+    }
+}
+
 @Throws(Exception::class)
 internal fun readTestFile(relativePath: String): ByteArray {
     val context: Context = InstrumentationRegistry.getInstrumentation().context
@@ -95,4 +106,4 @@
     }
     require(target is AssertionError) { "Unknown failure $target" }
     return ExpectFailure.assertThat(target)
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt
deleted file mode 100644
index 959232a..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker
-
-import android.graphics.Region
-import com.android.server.wm.flicker.traces.FlickerSubjectException
-import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
-import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
-import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runners.MethodSorters
-import java.lang.AssertionError
-
-/**
- * Contains [WindowManagerStateSubject] tests. To run this test: `atest
- * FlickerLibTest:WindowManagerStateSubjectTest`
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class WindowManagerStateSubjectTest {
-    private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
-
-    @Test
-    fun canDetectAboveAppWindowVisibility_isVisible() {
-        assertThat(trace)
-            .entry(9213763541297)
-            .isAboveAppWindow("NavigationBar")
-            .isAboveAppWindow("ScreenDecorOverlay")
-            .isAboveAppWindow("StatusBar")
-    }
-
-    @Test
-    fun canDetectAboveAppWindowVisibility_isInvisible() {
-        val subject = assertThat(trace).entry(9213763541297)
-        var failure = assertThrows(AssertionError::class.java) {
-            subject.isAboveAppWindow("pip-dismiss-overlay")
-        }
-        assertFailure(failure).factValue("Is Invisible").contains("pip-dismiss-overlay")
-
-        failure = assertThrows(AssertionError::class.java) {
-            subject.isAboveAppWindow("NavigationBar", isVisible = false)
-        }
-        assertFailure(failure).factValue("Is Visible").contains("NavigationBar")
-    }
-
-    @Test
-    fun canDetectWindowCoversAtLeastRegion_exactSize() {
-        val entry = assertThat(trace)
-            .entry(9213763541297)
-
-        entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1440, 171))
-        entry.frameRegion("com.google.android.apps.nexuslauncher")
-            .coversAtLeast(Region(0, 0, 1440, 2960))
-    }
-
-    @Test
-    fun canDetectWindowCoversAtLeastRegion_smallerRegion() {
-        val entry = assertThat(trace)
-            .entry(9213763541297)
-        entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 100, 100))
-        entry.frameRegion("com.google.android.apps.nexuslauncher")
-            .coversAtLeast(Region(0, 0, 100, 100))
-    }
-
-    @Test
-    fun canDetectWindowCoversAtLeastRegion_largerRegion() {
-        val subject = assertThat(trace).entry(9213763541297)
-        var failure = assertThrows(FlickerSubjectException::class.java) {
-            subject.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1441, 171))
-        }
-        assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))")
-
-        failure = assertThrows(FlickerSubjectException::class.java) {
-            subject.frameRegion("com.google.android.apps.nexuslauncher")
-                .coversAtLeast(Region(0, 0, 1440, 2961))
-        }
-        assertFailure(failure).factValue("Uncovered region")
-            .contains("SkRegion((0,2960,1440,2961))")
-    }
-
-    @Test
-    fun canDetectWindowCoversAtMostRegion_extactSize() {
-        val entry = assertThat(trace)
-            .entry(9213763541297)
-        entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1440, 171))
-        entry.frameRegion("com.google.android.apps.nexuslauncher")
-            .coversAtMost(Region(0, 0, 1440, 2960))
-    }
-
-    @Test
-    fun canDetectWindowCoversAtMostRegion_smallerRegion() {
-        val subject = assertThat(trace).entry(9213763541297)
-        var failure = assertThrows(FlickerSubjectException::class.java) {
-            subject.frameRegion("StatusBar").coversAtMost(Region(0, 0, 100, 100))
-        }
-        assertFailure(failure).factValue("Out-of-bounds region")
-            .contains("SkRegion((100,0,1440,100)(0,100,1440,171))")
-
-        failure = assertThrows(FlickerSubjectException::class.java) {
-            subject.frameRegion("com.google.android.apps.nexuslauncher")
-                .coversAtMost(Region(0, 0, 100, 100))
-        }
-        assertFailure(failure).factValue("Out-of-bounds region")
-            .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))")
-    }
-
-    @Test
-    fun canDetectWindowCoversAtMostRegion_largerRegion() {
-        val entry = assertThat(trace)
-            .entry(9213763541297)
-
-        entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1441, 171))
-        entry.frameRegion("com.google.android.apps.nexuslauncher")
-            .coversAtMost(Region(0, 0, 1440, 2961))
-    }
-
-    @Test
-    fun canDetectBelowAppWindowVisibility() {
-        assertThat(trace)
-            .entry(9213763541297)
-            .containsNonAppWindow("wallpaper")
-    }
-
-    @Test
-    fun canDetectAppWindowVisibility() {
-        assertThat(trace)
-            .entry(9213763541297)
-            .containsAppWindow("com.google.android.apps.nexuslauncher")
-
-        assertThat(trace)
-            .entry(9215551505798)
-            .containsAppWindow("com.android.chrome")
-    }
-
-    @Test
-    fun canFailWithReasonForVisibilityChecks_windowNotFound() {
-        val failure = assertThrows(FlickerSubjectException::class.java) {
-            assertThat(trace)
-                .entry(9213763541297)
-                .containsNonAppWindow("ImaginaryWindow")
-        }
-        assertFailure(failure).factValue("Could not find")
-            .contains("ImaginaryWindow")
-    }
-
-    @Test
-    fun canFailWithReasonForVisibilityChecks_windowNotVisible() {
-        val failure = assertThrows(FlickerSubjectException::class.java) {
-            assertThat(trace)
-                .entry(9213763541297)
-                .containsNonAppWindow("InputMethod")
-        }
-        assertFailure(failure).factValue("Is Invisible")
-            .contains("InputMethod")
-    }
-
-    @Test
-    fun canDetectAppZOrder() {
-        assertThat(trace)
-            .entry(9215551505798)
-            .containsAppWindow("com.google.android.apps.nexuslauncher", isVisible = true)
-            .showsAppWindowOnTop("com.android.chrome")
-    }
-
-    @Test
-    fun canFailWithReasonForZOrderChecks_windowNotOnTop() {
-        val failure = assertThrows(FlickerSubjectException::class.java) {
-            assertThat(trace)
-                .entry(9215551505798)
-                .showsAppWindowOnTop("com.google.android.apps.nexuslauncher")
-        }
-        assertFailure(failure)
-            .factValue("Found")
-            .contains("Splash Screen com.android.chrome")
-    }
-}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt
deleted file mode 100644
index d44c8ba..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2021 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.server.wm.flicker
-
-import com.android.server.wm.flicker.traces.FlickerSubjectException
-import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
-import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runners.MethodSorters
-
-/**
- * Contains [WindowManagerTraceSubject] tests. To run this test: `atest
- * FlickerLibTest:WindowManagerTraceSubjectTest`
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class WindowManagerTraceSubjectTest {
-    private val chromeTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
-    private val imeTrace by lazy { readWmTraceFromFile("wm_trace_ime.pb") }
-
-    @Test
-    fun testVisibleAppWindowForRange() {
-        assertThat(chromeTrace)
-            .showsAppWindowOnTop("NexusLauncherActivity")
-            .showsAboveAppWindow("ScreenDecorOverlay")
-            .forRange(9213763541297L, 9215536878453L)
-        assertThat(chromeTrace)
-            .showsAppWindowOnTop("com.android.chrome")
-            .showsAppWindow("NexusLauncherActivity")
-            .showsAboveAppWindow("ScreenDecorOverlay")
-            .then()
-            .showsAppWindowOnTop("com.android.chrome")
-            .hidesAppWindow("NexusLauncherActivity")
-            .showsAboveAppWindow("ScreenDecorOverlay")
-            .forRange(9215551505798L, 9216093628925L)
-    }
-
-    @Test
-    fun testCanTransitionInAppWindow() {
-        assertThat(chromeTrace)
-            .showsAppWindowOnTop("NexusLauncherActivity")
-            .showsAboveAppWindow("ScreenDecorOverlay")
-            .then()
-            .showsAppWindowOnTop("com.android.chrome")
-            .showsAboveAppWindow("ScreenDecorOverlay")
-            .forAllEntries()
-    }
-
-    @Test
-    fun testCanInspectBeginning() {
-        assertThat(chromeTrace)
-            .first()
-            .showsAppWindowOnTop("NexusLauncherActivity")
-            .isAboveAppWindow("ScreenDecorOverlay")
-    }
-
-    @Test
-    fun testCanInspectAppWindowOnTop() {
-        assertThat(chromeTrace)
-            .first()
-            .showsAppWindowOnTop("NexusLauncherActivity", "InvalidWindow")
-
-        val failure = assertThrows(FlickerSubjectException::class.java) {
-            assertThat(chromeTrace)
-                .first()
-                .showsAppWindowOnTop("AnotherInvalidWindow", "InvalidWindow")
-                .fail("Could not detect the top app window")
-        }
-        assertFailure(failure).factValue("Could not find").contains("InvalidWindow")
-    }
-
-    @Test
-    fun testCanInspectEnd() {
-        assertThat(chromeTrace)
-            .last()
-            .showsAppWindowOnTop("com.android.chrome")
-            .isAboveAppWindow("ScreenDecorOverlay")
-    }
-
-    @Test
-    fun testCanTransitionNonAppWindow() {
-        assertThat(imeTrace)
-            .skipUntilFirstAssertion()
-            .hidesNonAppWindow("InputMethod")
-            .then()
-            .showsNonAppWindow("InputMethod")
-            .forAllEntries()
-    }
-
-    @Test(expected = AssertionError::class)
-    fun testCanDetectOverlappingWindows() {
-        assertThat(imeTrace)
-            .noWindowsOverlap("InputMethod", "NavigationBar", "ImeActivity")
-            .forAllEntries()
-    }
-
-    @Test
-    fun testCanTransitionAboveAppWindow() {
-        assertThat(imeTrace)
-            .skipUntilFirstAssertion()
-            .hidesAboveAppWindow("InputMethod")
-            .then()
-            .showsAboveAppWindow("InputMethod")
-            .forAllEntries()
-    }
-
-    @Test
-    fun testCanTransitionBelowAppWindow() {
-        val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
-        assertThat(trace)
-            .skipUntilFirstAssertion()
-            .showsBelowAppWindow("Wallpaper")
-            .then()
-            .hidesBelowAppWindow("Wallpaper")
-            .forAllEntries()
-    }
-
-    @Test
-    fun testCanDetectVisibleWindowsMoreThanOneConsecutiveEntry() {
-        val trace = readWmTraceFromFile("wm_trace_valid_visible_windows.pb")
-        assertThat(trace).visibleWindowsShownMoreThanOneConsecutiveEntry().forAllEntries()
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerSubjectTest.kt
new file mode 100644
index 0000000..5fff302
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerSubjectTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.layers
+
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.traces.layers.LayerSubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.Size
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayerSubject] tests. To run this test:
+ * `atest FlickerLibTest:LayerSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayerSubjectTest {
+    @Test
+    fun exceptionContainsDebugInfoImaginary() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .first()
+                .layer("ImaginaryLayer", 0)
+                .exists()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("ImaginaryLayer")
+        Truth.assertThat(error).hasMessageThat().contains("What?")
+        Truth.assertThat(error).hasMessageThat().contains("Where?")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Layer name")
+    }
+
+    @Test
+    fun exceptionContainsDebugInfoConcrete() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                    .first()
+                    .subjects
+                    .first()
+                    .doesNotExist()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("What?")
+        Truth.assertThat(error).hasMessageThat().contains("Where?")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+    }
+
+    @Test
+    fun canTestAssertionsOnLayer() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        assertThat(layersTraceEntries)
+            .layer("SoundVizWallpaperV2", 26033)
+            .hasBufferSize(Size(1440, 2960))
+            .hasScalingMode(0)
+
+        assertThat(layersTraceEntries)
+            .layer("DoesntExist", 1)
+            .doesNotExist()
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerTraceEntrySubjectTest.kt
similarity index 77%
rename from libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt
rename to libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerTraceEntrySubjectTest.kt
index 76375b8..b8fe45a 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerTraceEntrySubjectTest.kt
@@ -14,11 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker
+package com.android.server.wm.flicker.layers
 
 import android.graphics.Region
+import com.android.server.wm.flicker.DOCKER_STACK_DIVIDER_COMPONENT
+import com.android.server.wm.flicker.IMAGINARY_COMPONENT
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.SIMPLE_APP_COMPONENT
+import com.android.server.wm.flicker.assertFailure
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.readLayerTraceFromFile
 import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
 import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.google.common.truth.Truth
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -36,28 +45,31 @@
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(layersTraceEntries)
                 .first()
-                .contains("ImaginaryLayer")
+                .visibleRegion(IMAGINARY_COMPONENT)
         }
-        Truth.assertThat(error).hasMessageThat().contains("Trace:")
-        Truth.assertThat(error).hasMessageThat().contains("Path: ")
-        Truth.assertThat(error).hasMessageThat().contains("Entry:")
+        Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className)
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains(FlickerSubject.ASSERTION_TAG)
     }
 
     @Test
     fun testCanInspectBeginning() {
         val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
         LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.first())
-            .isVisible("NavigationBar0#0")
-            .notContains("DockedStackDivider#0")
-            .isVisible("NexusLauncherActivity#0")
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .notContains(DOCKER_STACK_DIVIDER_COMPONENT)
+            .isVisible(LAUNCHER_COMPONENT)
     }
 
     @Test
     fun testCanInspectEnd() {
         val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
         LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.last())
-            .isVisible("NavigationBar0#0")
-            .isVisible("DockedStackDivider#0")
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isVisible(DOCKER_STACK_DIVIDER_COMPONENT)
     }
 
     // b/75276931
@@ -82,17 +94,16 @@
     // Visible region tests
     @Test
     fun canTestLayerVisibleRegion_layerDoesNotExist() {
-        val imaginaryLayer = "ImaginaryLayer"
         val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
         val expectedVisibleRegion = Region(0, 0, 1, 1)
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(trace).entry(937229257165)
-                .visibleRegion(imaginaryLayer)
+                .visibleRegion(IMAGINARY_COMPONENT)
                 .coversExactly(expectedVisibleRegion)
         }
         assertFailure(error)
             .factValue("Could not find")
-            .isEqualTo(imaginaryLayer)
+                .contains(IMAGINARY_COMPONENT.toWindowName())
     }
 
     @Test
@@ -101,7 +112,7 @@
         val expectedVisibleRegion = Region(0, 0, 1, 1)
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(trace).entry(937126074082)
-                .visibleRegion("DockedStackDivider#0")
+                .visibleRegion(DOCKER_STACK_DIVIDER_COMPONENT)
                 .coversExactly(expectedVisibleRegion)
         }
         assertFailure(error)
@@ -115,7 +126,7 @@
         val expectedVisibleRegion = Region(0, 0, 1, 1)
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(trace).entry(935346112030)
-                .visibleRegion("SimpleActivity#0")
+                .visibleRegion(SIMPLE_APP_COMPONENT)
                 .coversExactly(expectedVisibleRegion)
         }
         assertFailure(error)
@@ -129,7 +140,7 @@
         val expectedVisibleRegion = Region(0, 0, 1440, 99)
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(trace).entry(937126074082)
-                .visibleRegion("StatusBar")
+                .visibleRegion(FlickerComponentName.STATUS_BAR)
                 .coversExactly(expectedVisibleRegion)
         }
         assertFailure(error)
@@ -142,7 +153,7 @@
         val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
         val expectedVisibleRegion = Region(0, 0, 1080, 145)
         LayersTraceSubject.assertThat(trace).entry(90480846872160)
-            .visibleRegion("StatusBar")
+            .visibleRegion(FlickerComponentName.STATUS_BAR)
             .coversExactly(expectedVisibleRegion)
     }
 
@@ -151,7 +162,7 @@
         val trace = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb")
         val error = assertThrows(AssertionError::class.java) {
             LayersTraceSubject.assertThat(trace).entry(252794268378458)
-                .isVisible("com.android.server.wm.flicker.testapp")
+                .isVisible(SIMPLE_APP_COMPONENT)
         }
         assertFailure(error)
             .factValue("Is Invisible")
@@ -167,7 +178,8 @@
         entry.visibleRegion(useCompositionEngineRegionOnly = false)
             .coversExactly(Region(0, 0, 1440, 2960))
 
-        entry.visibleRegion("InputMethod#0", useCompositionEngineRegionOnly = false)
+        entry.visibleRegion(FlickerComponentName.IME,
+            useCompositionEngineRegionOnly = false)
             .coversExactly(Region(0, 171, 1440, 2960))
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceEntryTest.kt
similarity index 92%
rename from libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt
rename to libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceEntryTest.kt
index 616fc47..670bba4 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceEntryTest.kt
@@ -14,15 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker
+package com.android.server.wm.flicker.layers
 
-import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.flicker.IMAGINARY_COMPONENT
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readLayerTraceFromFile
 import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
 import com.google.common.truth.Truth
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runners.MethodSorters
-import kotlin.AssertionError
 
 /**
  * Contains [LayerTraceEntry] tests. To run this test: `atest
@@ -36,11 +38,12 @@
         val error = assertThrows(AssertionError::class.java) {
             assertThat(layersTraceEntries)
                 .first()
-                .contains("ImaginaryLayer")
+                .contains(IMAGINARY_COMPONENT)
         }
-        Truth.assertThat(error).hasMessageThat().contains("Trace:")
-        Truth.assertThat(error).hasMessageThat().contains("Path: ")
-        Truth.assertThat(error).hasMessageThat().contains("Entry:")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace end")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
     }
 
     @Test
@@ -107,8 +110,8 @@
         Truth.assertThat(trace.entries.first().timestamp).isEqualTo(922839428857)
         Truth.assertThat(trace.entries.last().timestamp).isEqualTo(941432656959)
         Truth.assertThat(trace.entries.first().flattenedLayers).asList().hasSize(57)
-        val layers = trace.entries.first().rootLayers
-        Truth.assertThat(layers[0].children).hasSize(3)
+        val layers = trace.entries.first().children
+        Truth.assertThat(layers[0].children).asList().hasSize(3)
         Truth.assertThat(layers[1].children).isEmpty()
     }
 
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceSubjectTest.kt
new file mode 100644
index 0000000..9ef6a03
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceSubjectTest.kt
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.layers
+
+import androidx.test.filters.FlakyTest
+import com.android.server.wm.flicker.DOCKER_STACK_DIVIDER_COMPONENT
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.SIMPLE_APP_COMPONENT
+import com.android.server.wm.flicker.assertFailure
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.parser.minus
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayersTraceSubject] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceSubjectTest {
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .isEmpty()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace end")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+    }
+
+    @Test
+    fun testCanDetectEmptyRegionFromLayerTrace() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        try {
+            assertThat(layersTraceEntries)
+                .coversAtLeast(DISPLAY_REGION)
+                .forAllEntries()
+            error("Assertion should not have passed")
+        } catch (e: Throwable) {
+            assertFailure(e).factValue("Region to test").contains(DISPLAY_REGION.toString())
+            assertFailure(e).factValue("Uncovered region").contains("SkRegion((0,1440,1440,2880))")
+        }
+    }
+
+    @Test
+    fun testCanInspectBeginning() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .first()
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .notContains(DOCKER_STACK_DIVIDER_COMPONENT)
+            .isVisible(LAUNCHER_COMPONENT)
+    }
+
+    @Test
+    fun testCanInspectEnd() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .last()
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isVisible(DOCKER_STACK_DIVIDER_COMPONENT)
+    }
+
+    @Test
+    fun testAssertionsOnRange() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+
+        assertThat(layersTraceEntries)
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isInvisible(DOCKER_STACK_DIVIDER_COMPONENT)
+            .forRange(90480846872160L, 90480994138424L)
+
+        assertThat(layersTraceEntries)
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isVisible(DOCKER_STACK_DIVIDER_COMPONENT)
+            .forRange(90491795074136L, 90493757372977L)
+    }
+
+    @Test
+    fun testCanDetectChangingAssertions() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .notContains(DOCKER_STACK_DIVIDER_COMPONENT)
+            .then()
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isInvisible(DOCKER_STACK_DIVIDER_COMPONENT)
+            .then()
+            .isVisible(FlickerComponentName.NAV_BAR)
+            .isVisible(DOCKER_STACK_DIVIDER_COMPONENT)
+            .forAllEntries()
+    }
+
+    @FlakyTest
+    @Test
+    fun testCanDetectIncorrectVisibilityFromLayerTrace() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .isVisible(SIMPLE_APP_COMPONENT)
+                .then()
+                .isInvisible(SIMPLE_APP_COMPONENT)
+                .forAllEntries()
+        }
+
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("layers_trace_invalid_layer_visibility.pb")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("2d22h13m14s303ms")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("!isVisible")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("com.android.server.wm.flicker.testapp/" +
+                "com.android.server.wm.flicker.testapp.SimpleActivity#0 is visible")
+    }
+
+    @Test
+    fun testCanDetectInvalidVisibleLayerForMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_visible_layers.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry()
+                .forAllEntries()
+            error("Assertion should not have passed")
+        }
+
+        Truth.assertThat(error).hasMessageThat().contains("2d18h35m56s397ms")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("StatusBar#0")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("is not visible for 2 entries")
+    }
+
+    private fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(trace: LayersTrace) {
+        assertThat(trace)
+            .visibleLayersShownMoreThanOneConsecutiveEntry()
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry() {
+        testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(
+            readLayerTraceFromFile("layers_trace_snapshot_visible.pb"))
+    }
+
+    @Test
+    fun testCanIgnoreLayerEqualNameInVisibleLayersMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile(
+                "layers_trace_invalid_visible_layers.pb")
+        assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry(
+                    listOf(FlickerComponentName.STATUS_BAR))
+                .forAllEntries()
+    }
+
+    @Test
+    fun testCanIgnoreLayerShorterNameInVisibleLayersMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile(
+                "one_visible_layer_launcher_trace.pb")
+        val launcherComponent = FlickerComponentName("com.google.android.apps.nexuslauncher",
+                "com.google.android.apps.nexuslauncher.NexusLauncherActivity#1")
+        assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry(listOf(launcherComponent))
+                .forAllEntries()
+    }
+
+    private fun detectRootLayer(fileName: String) {
+        val layersTrace = readLayerTraceFromFile(fileName)
+        for (entry in layersTrace.entries) {
+            val rootLayers = entry.children
+            Truth.assertWithMessage("Does not have any root layer")
+                    .that(rootLayers.size)
+                    .isGreaterThan(0)
+            val firstParentId = rootLayers.first().parentId
+            Truth.assertWithMessage("Has multiple root layers")
+                    .that(rootLayers.all { it.parentId == firstParentId })
+                    .isTrue()
+        }
+    }
+
+    @Test
+    fun testCanDetectRootLayer() {
+        detectRootLayer("layers_trace_root.pb")
+    }
+
+    @Test
+    fun testCanDetectRootLayerAOSP() {
+        detectRootLayer("layers_trace_root_aosp.pb")
+    }
+
+    @Test
+    fun canTestLayerOccludedByAppLayerIsNotVisible() {
+        val trace = readLayerTraceFromFile("layers_trace_occluded.pb")
+        val entry = assertThat(trace).entry(1700382131522L)
+        entry.isVisible(SIMPLE_APP_COMPONENT)
+    }
+
+    @Test
+    fun testCanDetectLayerExpanding() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_openchrome.pb")
+        val animation = assertThat(layersTraceEntries).layers("animation-leash of app_transition#0")
+        // Obtain the area of each layer and checks if the next area is
+        // greater or equal to the previous one
+        val areas = animation.map {
+            val region = it.layer?.visibleRegion ?: Region()
+            val area = region.width * region.height
+            area
+        }
+        val expanding = areas.zipWithNext { currentArea, nextArea ->
+            nextArea >= currentArea
+        }
+
+        Truth.assertWithMessage("Animation leash should be expanding")
+            .that(expanding.all { it })
+            .isTrue()
+    }
+
+    @Test
+    fun checkVisibleRegionAppMinusPipLayer() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_pip_wmshell.pb")
+        val subject = assertThat(layersTraceEntries).last()
+
+        try {
+            subject.visibleRegion(FIXED_APP).coversExactly(DISPLAY_REGION_ROTATED)
+            error("Layer is partially covered by a Pip layer and should " +
+                "not cover the device screen")
+        } catch (e: AssertionError) {
+            val pipRegion = subject.visibleRegion(PIP_APP).region
+            val expectedWithoutPip = DISPLAY_REGION_ROTATED.minus(pipRegion)
+            subject.visibleRegion(FIXED_APP)
+                    .coversExactly(expectedWithoutPip)
+        }
+    }
+
+    @Test
+    fun checkVisibleRegionAppPlusPipLayer() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_pip_wmshell.pb")
+        val subject = assertThat(layersTraceEntries).last()
+        val pipRegion = subject.visibleRegion(PIP_APP).region
+        subject.visibleRegion(FIXED_APP)
+                .plus(pipRegion)
+                .coversExactly(DISPLAY_REGION_ROTATED)
+    }
+
+    companion object {
+        private val DISPLAY_REGION = android.graphics.Region(0, 0, 1440, 2880)
+        private val DISPLAY_REGION_ROTATED = Region(0, 0, 2160, 1080)
+        private const val SHELL_APP_PACKAGE = "com.android.wm.shell.flicker.testapp"
+        private val FIXED_APP = FlickerComponentName(SHELL_APP_PACKAGE,
+                "$SHELL_APP_PACKAGE.FixedActivity")
+        private val PIP_APP = FlickerComponentName(SHELL_APP_PACKAGE,
+            "$SHELL_APP_PACKAGE.PipActivity")
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt
new file mode 100644
index 0000000..bac0ac4
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.layers
+
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayersTrace] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceTest {
+    private fun detectRootLayer(fileName: String) {
+        val layersTrace = readLayerTraceFromFile(fileName)
+        for (entry in layersTrace.entries) {
+            val rootLayers = entry.children
+            Truth.assertWithMessage("Does not have any root layer")
+                .that(rootLayers.size)
+                .isGreaterThan(0)
+            val firstParentId = rootLayers.first().parentId
+            Truth.assertWithMessage("Has multiple root layers")
+                .that(rootLayers.all { it.parentId == firstParentId })
+                .isTrue()
+        }
+    }
+
+    @Test
+    fun testCanDetectRootLayer() {
+        detectRootLayer("layers_trace_root.pb")
+    }
+
+    @Test
+    fun testCanDetectRootLayerAOSP() {
+        detectRootLayer("layers_trace_root_aosp.pb")
+    }
+
+    @Test
+    fun testCanParseTraceWithoutHWC() {
+        val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb")
+        layersTrace.forEach { entry ->
+            Truth.assertWithMessage("Should have visible layers in all trace entries")
+                .that(entry.visibleLayers).asList()
+                .isNotEmpty()
+        }
+    }
+
+    @Test
+    fun canParseFromDumpWithDisplay() {
+        val trace = readLayerTraceFromFile("layers_dump_with_display.pb")
+        Truth.assertWithMessage("Dump is not empty")
+            .that(trace)
+            .isNotEmpty()
+        Truth.assertWithMessage("Dump contains display is not empty")
+            .that(trace.first().displays)
+            .asList()
+            .isNotEmpty()
+    }
+
+    @Test
+    fun canTestLayerOccludedBy_appLayerHasVisibleRegion() {
+        val trace = readLayerTraceFromFile("layers_trace_occluded.pb")
+        val entry = trace.getEntry(1700382131522L)
+        val layer = entry.getLayerWithBuffer(
+                "com.android.server.wm.flicker.testapp.SimpleActivity#0")
+        Truth.assertWithMessage("App should be visible")
+                .that(layer?.visibleRegion?.isEmpty).isFalse()
+        Truth.assertWithMessage("App should visible region")
+                .that(layer?.visibleRegion?.toString())
+                .contains("(346, 1583) - (1094, 2839)")
+
+        val splashScreenLayer = entry.getLayerWithBuffer(
+                "Splash Screen com.android.server.wm.flicker.testapp.SimpleActivity#0")
+        Truth.assertWithMessage("Splash screen should be visible")
+                .that(layer?.visibleRegion?.isEmpty).isFalse()
+        Truth.assertWithMessage("Splash screen visible region")
+                .that(layer?.visibleRegion?.toString())
+                .contains("(346, 1583) - (1094, 2839)")
+    }
+
+    @Test
+    fun canTestLayerOccludedBy_appLayerIsOccludedBySplashScreen() {
+        val layerName = "com.android.server.wm.flicker.testapp.SimpleActivity#0"
+        val trace = readLayerTraceFromFile("layers_trace_occluded.pb")
+        val entry = trace.getEntry(1700382131522L)
+        val layer = entry.getLayerWithBuffer(layerName)
+        val occludedBy = layer?.occludedBy ?: emptyArray()
+        val partiallyOccludedBy = layer?.partiallyOccludedBy ?: emptyArray()
+        Truth.assertWithMessage("Layer $layerName should not be occluded")
+                .that(occludedBy).isEmpty()
+        Truth.assertWithMessage("Layer $layerName should be partially occluded")
+                .that(partiallyOccludedBy).isNotEmpty()
+        Truth.assertWithMessage("Layer $layerName should be partially occluded")
+                .that(partiallyOccludedBy.joinToString())
+                .contains("Splash Screen com.android.server.wm.flicker.testapp#0 buffer:w:1440, " +
+                        "h:3040, stride:1472, format:1 frame#1 visible:(346, 1583) - (1094, 2839)")
+    }
+
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(layersTraceEntries)
+                    .isEmpty()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+    }
+
+    @Test
+    fun canFilter() {
+        val trace = readLayerTraceFromFile("layers_trace_openchrome.pb")
+        val splitlayersTrace = trace.filter(71607477186189, 71607812120180)
+
+        Truth.assertThat(splitlayersTrace).isNotEmpty()
+
+        Truth.assertThat(splitlayersTrace.entries.first().timestamp).isEqualTo(71607477186189)
+        Truth.assertThat(splitlayersTrace.entries.last().timestamp).isEqualTo(71607812120180)
+    }
+
+    @Test
+    fun canFilter_wrongTimestamps() {
+        val trace = readLayerTraceFromFile("layers_trace_openchrome.pb")
+        val splitLayersTrace = trace.filter(9213763541297, 9215895891561)
+
+        Truth.assertThat(splitLayersTrace).isEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
index c8ac97d..37001be 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
@@ -63,7 +63,7 @@
     @Test
     fun videoCanBeSaved() {
         mScreenRecorder.start()
-        SystemClock.sleep(100)
+        SystemClock.sleep(3000)
         mScreenRecorder.stop()
         val builder = FlickerRunResult.Builder()
         mScreenRecorder.save("test", builder)
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
index 39f70a5..fa7aee4 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
@@ -20,7 +20,8 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.server.wm.flicker.FlickerRunResult
 import com.android.server.wm.flicker.getDefaultFlickerOutputDir
-import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.common.DeviceTraceDump
+import com.android.server.wm.traces.parser.DeviceDumpParser
 import com.google.common.io.Files
 import com.google.common.truth.Truth
 import org.junit.After
@@ -81,19 +82,19 @@
         savedTrace = getTraceFile(result) ?: error("Could not find saved trace file")
         val testFile = savedTrace.toFile()
         Truth.assertThat(testFile.exists()).isTrue()
-        val calculatedChecksum = TraceMonitor.calculateChecksum(savedTrace)
-        Truth.assertThat(calculatedChecksum).isEqualTo(traceMonitor.checksum)
         val trace = Files.toByteArray(testFile)
         Truth.assertThat(trace.size).isGreaterThan(0)
         assertTrace(trace)
     }
 
-    private fun validateTrace(dump: DeviceStateDump) {
+    private fun validateTrace(dump: DeviceTraceDump) {
         Truth.assertWithMessage("Could not obtain SF trace")
-            .that(dump.layersTrace?.entries)
+            .that(dump.layersTrace?.entries ?: emptyArray())
+            .asList()
             .isNotEmpty()
         Truth.assertWithMessage("Could not obtain WM trace")
-            .that(dump.wmTrace?.entries)
+            .that(dump.wmTrace?.entries ?: emptyArray())
+            .asList()
             .isNotEmpty()
     }
 
@@ -114,7 +115,7 @@
             device.pressRecentApps()
         }
 
-        val dump = DeviceStateDump.fromTrace(trace.first, trace.second)
+        val dump = DeviceDumpParser.fromTrace(trace.first, trace.second)
         this.validateTrace(dump)
     }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt
new file mode 100644
index 0000000..99c08ce
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readTagTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.tags.Transition
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [AssertionEngine] tests. To run this test:
+ * `atest FlickerLibTest:AssertionEngineTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AssertionEngineTest {
+    private val assertionEngine = AssertionEngine(emptyList()) { }
+    private val wmTrace by lazy {
+        readWmTraceFromFile("assertors/AppLaunchAndRotationsWindowManagerTrace.winscope")
+    }
+    private val layersTrace by lazy {
+        readLayerTraceFromFile("assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope")
+    }
+    private val tagTrace by lazy {
+        readTagTraceFromFile("assertors/AppLaunchAndRotationsTagTrace.winscope")
+    }
+    private val transitionTags by lazy { assertionEngine.getTransitionTags(tagTrace) }
+
+    @Test
+    fun canExtractTransitionTags() {
+        Truth.assertThat(transitionTags).isNotEmpty()
+        Truth.assertThat(transitionTags.size).isEqualTo(3)
+    }
+
+    @Test
+    fun canSplitTraces_singleTag() {
+        val blocks = transitionTags
+            .filter { it.tag.transition == Transition.APP_LAUNCH }
+            .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) }
+
+        Truth.assertThat(blocks).isNotEmpty()
+        Truth.assertThat(blocks.size).isEqualTo(1)
+
+        val entries = blocks.first().first.entries
+        Truth.assertThat(entries.first().timestamp).isEqualTo(294063112453765)
+        Truth.assertThat(entries.last().timestamp).isEqualTo(294063379330458)
+    }
+
+    @Test
+    fun canSplitLayersTrace_mergedTags() {
+        val blocks = transitionTags
+            .filter { it.tag.transition == Transition.ROTATION }
+            .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) }
+
+        Truth.assertThat(blocks).isNotEmpty()
+        Truth.assertThat(blocks.size).isEqualTo(2)
+
+        val entries = blocks.last().second.entries
+        Truth.assertThat(entries.first().timestamp).isEqualTo(294064497020048)
+        Truth.assertThat(entries.last().timestamp).isEqualTo(294064981192909)
+    }
+
+    @Test
+    fun canSplitLayersTrace_noTags() {
+        val blocks = transitionTags
+            .filter { it.tag.transition == Transition.APP_CLOSE }
+            .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) }
+
+        Truth.assertThat(blocks).isEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt
new file mode 100644
index 0000000..44d4d6e
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import com.android.server.wm.flicker.FlickerErrorProto
+import com.android.server.wm.flicker.FlickerErrorStateProto
+import com.android.server.wm.flicker.FlickerErrorTraceProto
+import com.android.server.wm.traces.parser.errors.ErrorTraceParserUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [ErrorTraceParserUtil] and [FlickerErrorProto] tests. To run this test: `atest
+ * FlickerLibTest:ErrorParserTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ErrorParserTest {
+    private val error = FlickerErrorProto.newBuilder()
+            .setLayerId(1)
+            .setTaskId(2)
+            .setWindowToken("token")
+            .setMessage("Error!")
+            .setStacktrace("stacktrace of error")
+            .setAssertionName("LayerIsVisibleAtStart")
+            .build()
+    private val state = FlickerErrorStateProto.newBuilder()
+            .setTimestamp(100)
+            .addErrors(error)
+            .build()
+    private val trace = FlickerErrorTraceProto.newBuilder()
+            .addStates(state)
+            .build()
+    private val traceBytes = trace.toByteArray()
+
+    @Test
+    fun canParseErrors() {
+        val errorTrace = ErrorTraceParserUtil.parseFromTrace(traceBytes)
+        val errorState = errorTrace.entries.first()
+        val error = errorState.errors.first()
+
+        assertThat(errorState.timestamp).isEqualTo(100)
+        assertThat(error.layerId).isEqualTo(1)
+        assertThat(error.taskId).isEqualTo(2)
+        assertThat(error.message).isEqualTo("Error!")
+        assertThat(error.stacktrace).isEqualTo("stacktrace of error")
+        assertThat(error.windowToken).isEqualTo("token")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt
new file mode 100644
index 0000000..a69af64
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.helpers.SampleAppHelper
+import com.android.server.wm.flicker.rules.WMFlickerServiceRule
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.FixMethodOrder
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains a mock test for [WMFlickerServiceRule].
+ *
+ * To run this test: `atest FlickerLibTest:FassMockTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class FassMockTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val dummyAppHelper = SampleAppHelper(instrumentation)
+
+    @get:Rule
+    val rule = WMFlickerServiceRuleTest()
+
+    @Test
+    fun startServiceTest() {
+        val device = UiDevice.getInstance(instrumentation)
+        val wmHelper = WindowManagerStateHelper(instrumentation)
+        device.wakeUp()
+        device.pressHome()
+        wmHelper.waitForHomeActivityVisible()
+        dummyAppHelper.launchViaIntent(wmHelper)
+    }
+
+    companion object {
+        private val DUMMY_APP = FlickerComponentName("com.google.android.apps.messaging",
+            "com.google.android.apps.messaging.ui.ConversationListActivity")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt
new file mode 100644
index 0000000..fdc59bf
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.helpers.SampleAppHelper
+import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.rules.WMFlickerServiceRule
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.FixMethodOrder
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains a rotation mock test for [WMFlickerServiceRule].
+ *
+ * To run this test: `atest FlickerLibTest:RotationMockTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RotationMockTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val dummyAppHelper = SampleAppHelper(instrumentation)
+
+    @get:Rule
+    val rule = WMFlickerServiceRuleTest()
+
+    @Test
+    fun startRotationServiceTest() {
+        val device = UiDevice.getInstance(instrumentation)
+        val wmHelper = WindowManagerStateHelper(instrumentation)
+
+        device.wakeUpAndGoToHomeScreen()
+        wmHelper.waitForHomeActivityVisible()
+        dummyAppHelper.launchViaIntent(wmHelper)
+        device.setOrientationLeft()
+        instrumentation.uiAutomation.syncInputTransactions()
+        device.setOrientationNatural()
+        instrumentation.uiAutomation.syncInputTransactions()
+    }
+
+    companion object {
+        private val DUMMY_APP = FlickerComponentName("com.google.android.apps.messaging",
+                "com.google.android.apps.messaging.ui.ConversationListActivity")
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt
new file mode 100644
index 0000000..716d7cb
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import com.android.server.wm.flicker.FlickerTagProto
+import com.android.server.wm.flicker.FlickerTagStateProto
+import com.android.server.wm.flicker.FlickerTagTraceProto
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.parser.tags.TagTraceParserUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [TagTraceParserUtil] and [FlickerTagProto] tests. To run this test: `atest
+ * FlickerLibTest:TagParserTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class TagParserTest {
+    private val tag = FlickerTagProto.newBuilder()
+                .setLayerId(1)
+                .setTaskId(2)
+                .setIsStartTag(true)
+                .setId(123)
+                .setTransition(FlickerTagProto.Transition.APP_CLOSE)
+                .setWindowToken("token")
+                .build()
+    private val state = FlickerTagStateProto.newBuilder()
+                .setTimestamp(100)
+                .addTags(tag)
+                .build()
+    private val trace = FlickerTagTraceProto.newBuilder()
+                .addStates(state)
+                .build()
+    private val traceBytes = trace.toByteArray()
+
+    @Test
+    fun canParseTags() {
+        val tagTrace = TagTraceParserUtil.parseFromTrace(traceBytes)
+        val tagState = tagTrace.entries.first()
+        val tag = tagState.tags.first()
+
+        assertThat(tagState.timestamp).isEqualTo(100)
+        assertThat(tag.layerId).isEqualTo(1)
+        assertThat(tag.taskId).isEqualTo(2)
+        assertThat(tag.id).isEqualTo(123)
+        assertThat(tag.isStartTag).isTrue()
+        assertThat(tag.transition).isEqualTo(Transition.APP_CLOSE)
+        assertThat(tag.windowToken).isEqualTo("token")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt
new file mode 100644
index 0000000..efddbbd
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.helpers.SampleAppHelper
+import com.android.server.wm.flicker.monitor.withTracing
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasLayersAnimating
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isWMStateComplete
+import com.android.server.wm.traces.common.service.TaggingEngine
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class TraceIsTaggableTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val device = UiDevice.getInstance(instrumentation)
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+
+    @Test
+    fun canCreateTagsFromDeviceTrace() {
+
+        // Generates trace of opening the messaging application from home screen
+        val trace = withTracing {
+            device.pressHome()
+            SampleAppHelper(instrumentation).launchViaIntent(wmHelper)
+
+            // Wait until transition is fully completed
+            WindowManagerStateHelper().waitFor(
+                hasLayersAnimating().negate(),
+                isAppTransitionIdle(/* default display */ 0),
+                isWMStateComplete()
+            )
+        }
+
+        val engine = TaggingEngine(
+            requireNotNull(trace.wmTrace),
+            requireNotNull(trace.layersTrace)
+        ) { }
+
+        val tagStates = engine.run().entries
+        Truth.assertThat(tagStates.size).isEqualTo(2)
+
+        val startTag = tagStates.first().tags
+        val endTag = tagStates.last().tags
+        Truth.assertThat(startTag.size).isEqualTo(1)
+        Truth.assertThat(endTag.size).isEqualTo(1)
+
+        Truth.assertThat(startTag.first().transition).isEqualTo(Transition.APP_LAUNCH)
+        Truth.assertThat(endTag.first().transition).isEqualTo(Transition.APP_LAUNCH)
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt
new file mode 100644
index 0000000..3de48c9
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec
+import org.junit.FixMethodOrder
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WMFlickerServiceRuleForTestSpecTest(private val testSpec: FlickerTestParameter) {
+    @get:Rule
+    val flickerRule = WMFlickerServiceRuleForTestSpec(testSpec)
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @FlickerBuilderProvider
+    fun emptyFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            transitions {
+                device.wakeUpAndGoToHomeScreen()
+                wmHelper.waitForAppTransitionIdle()
+            }
+        }
+    }
+
+    @Test
+    fun runAssertion() {
+        flickerRule.checkPresubmitAssertions()
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                .getConfigNonRotationTests(repetitions = 1)
+                .take(1)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt
new file mode 100644
index 0000000..6672ad0
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service
+
+import com.android.server.wm.flicker.rules.WMFlickerServiceRule
+import com.google.common.truth.Truth
+import org.junit.runner.Description
+
+/**
+ * An extension for [WMFlickerServiceRule] checking that the traces are collected.
+ */
+class WMFlickerServiceRuleTest : WMFlickerServiceRule() {
+    override fun finished(description: Description?) {
+        super.finished(description)
+
+        Truth.assertThat(wmTrace).isNotEmpty()
+        Truth.assertThat(layersTrace).isNotEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt
new file mode 100644
index 0000000..ea6350d
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readTestFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains tests for App Launch assertions. To run this test:
+ * `atest FlickerLibTest:AppLaunchAssertionsTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AppLaunchAssertionsTest {
+    private val jsonByteArray = readTestFile("assertors/config.json")
+    private val assertions =
+        AssertionConfigParser.parseConfigFile(String(jsonByteArray))
+            .filter { it.transitionType == Transition.APP_LAUNCH }
+
+    private val appLaunchAssertor = TransitionAssertor(assertions) { }
+
+    @Test
+    fun testValidAppLaunchTraces() {
+        val wmTrace = readWmTraceFromFile(
+            "assertors/appLaunch/WindowManagerTrace.winscope")
+        val layersTrace = readLayerTraceFromFile(
+            "assertors/appLaunch/SurfaceFlingerTrace.winscope")
+        val errorTrace = appLaunchAssertor.analyze(VALID_APP_LAUNCH_TAG, wmTrace, layersTrace)
+
+        Truth.assertThat(errorTrace).isEmpty()
+    }
+
+    @Test
+    fun testInvalidAppLaunchTraces() {
+        val wmTrace = readWmTraceFromFile(
+            "assertors/appLaunch/WindowManagerInvalidTrace.winscope")
+        val layersTrace = readLayerTraceFromFile(
+            "assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope")
+        val errorTrace = appLaunchAssertor.analyze(INVALID_APP_LAUNCH_TAG, wmTrace, layersTrace)
+
+        Truth.assertThat(errorTrace).isNotEmpty()
+        Truth.assertThat(errorTrace.entries.size).isEqualTo(1)
+    }
+
+    companion object {
+        private val VALID_APP_LAUNCH_TAG = Tag(1, Transition.APP_LAUNCH, true,
+            windowToken = "e91fbda")
+        private val INVALID_APP_LAUNCH_TAG = Tag(2, Transition.APP_LAUNCH, true,
+            windowToken = "ffc3b1f")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt
new file mode 100644
index 0000000..e4a3615
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.flicker.readTestFile
+import com.android.server.wm.traces.common.tags.Transition
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [AssertionConfigParser] tests. To run this test:
+ * `atest FlickerLibTest:AssertionConfigParserTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AssertionConfigParserTest {
+
+    @Test
+    fun canParseConfigFile() {
+        val jsonByteArray = readTestFile("assertors/assertionsConfig.json")
+        val assertionConfigurations = AssertionConfigParser.parseConfigFile(String(jsonByteArray))
+        Truth.assertThat(assertionConfigurations).hasSize(23)
+        Truth.assertThat(assertionConfigurations.first().transitionType)
+            .isEqualTo(Transition.ROTATION)
+        Truth.assertThat(assertionConfigurations.last().transitionType)
+            .isEqualTo(Transition.APP_LAUNCH)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt
new file mode 100644
index 0000000..d0f7ea0
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.assertors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readTestFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.tags.Tag
+import com.android.server.wm.traces.common.tags.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains tests for rotation assertions. To run this test:
+ * `atest FlickerLibTest:RotationAssertionsTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RotationAssertionsTest {
+    private val jsonByteArray = readTestFile("assertors/config.json")
+    private val assertions =
+        AssertionConfigParser.parseConfigFile(String(jsonByteArray))
+            .filter { it.transitionType == Transition.ROTATION }
+
+    private val rotationAssertor = TransitionAssertor(assertions) { }
+
+    @Test
+    fun testValidRotationWmTrace() {
+        val wmTrace = readWmTraceFromFile("assertors/rotation/WindowManagerTrace.winscope")
+        val layersTrace = readLayerTraceFromFile("assertors/rotation/SurfaceFlingerTrace.winscope")
+        val errorTrace = rotationAssertor.analyze(ROTATION_TAG, wmTrace, layersTrace)
+
+        Truth.assertThat(errorTrace).isEmpty()
+    }
+
+    @Test
+    fun testValidRotationLayersTrace() {
+        val trace = readLayerTraceFromFile("assertors/rotation/SurfaceFlingerTrace.winscope")
+        val errorTrace = rotationAssertor.analyze(ROTATION_TAG, EMPTY_WM_TRACE, trace)
+
+        Truth.assertThat(errorTrace).isEmpty()
+    }
+
+    @Test
+    fun testInvalidRotationLayersTrace() {
+        val trace = readLayerTraceFromFile(
+            "assertors/rotation/SurfaceFlingerInvalidTrace.winscope")
+        val errorTrace = rotationAssertor.analyze(ROTATION_TAG, EMPTY_WM_TRACE, trace)
+
+        Truth.assertThat(errorTrace).isNotEmpty()
+        Truth.assertThat(errorTrace.entries.size).isEqualTo(1)
+    }
+
+    companion object {
+        private val EMPTY_WM_TRACE = WindowManagerTrace(emptyArray(), source = "")
+        private val ROTATION_TAG = Tag(1, Transition.ROTATION, true)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt
new file mode 100644
index 0000000..e0b55f8
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.AppCloseProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [AppCloseProcessor] tests. To run this test:
+ * `atest FlickerLibTest:AppCloseProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AppCloseProcessorTest {
+    private val processor = AppCloseProcessor { }
+
+    private val tagAppCloseByBackButton by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/appclose/backbutton/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagAppCloseBySwipeUp by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/appclose/swipeup/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagAppCloseBySwitchingApps by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/appclose/switchapp/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsAppCloseByClosingRotatedApp by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/appclose/rotated/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsColdAppLaunch by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/cold/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsWarmAppLaunch by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/warm/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesAppCloseTagsByPressingBackButton() {
+        val tagTrace = tagAppCloseByBackButton
+        Truth.assertWithMessage("Should have 2 app close tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 9295108146952 // 0d2h34m55s108ms
+        val endTagTimestamp = 9295480256103 // 0d2h34m55s480ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesAppCloseTagsBySwipe() {
+        val tagTrace = tagAppCloseBySwipeUp
+        Truth.assertWithMessage("Should have 2 app close tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 9320574596259 // 0d2h35m20s578ms
+        val endTagTimestamp = 9321301178051 // 0d2h35m21s301ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesAppCloseTagsBySwitchingApps() {
+        val tagTrace = tagAppCloseBySwitchingApps
+        Truth.assertWithMessage("Should have 2 app close tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 4129701437903 // 0d1h8m49s701ms
+        val endTagTimestamp = 4132063745690 // 0d1h8m52s52ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesAppCloseTagsWhenAppRotated90() {
+        val tagTrace = tagsAppCloseByClosingRotatedApp
+        Truth.assertWithMessage("Should have 2 app close tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 343127388040903 // 3d23h18m47s388ms
+        val endTagTimestamp = 343128129419414 // 3d23h18m48s129ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesNoTagsOnColdAppLaunch() {
+        val tagTrace = tagsColdAppLaunch
+        Truth.assertWithMessage("Should have 0 app launch tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+
+    @Test
+    fun generatesNoTagsOnWarmAppLaunch() {
+        val tagTrace = tagsWarmAppLaunch
+        Truth.assertWithMessage("Should have 0 app launch tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt
new file mode 100644
index 0000000..73da69f
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.AppLaunchProcessor
+import com.google.common.truth.Truth
+import org.junit.Test
+
+/**
+ * Contains [AppLaunchProcessor] tests. To run this test:
+ * `atest FlickerLibTest:AppLaunchProcessorTest`
+ */
+class AppLaunchProcessorTest {
+    private val processor = AppLaunchProcessor { }
+
+    /**
+     * Scenarios expecting tags
+     */
+    private val tagsColdAppLaunch by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/cold/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsWarmAppLaunch by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/warm/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsAppLaunchByIntent by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/intent/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsAppLaunchWithRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/applaunch/withrot/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    /**
+     * Scenarios expecting no tags
+     */
+    private val tagsComposeNewMessage by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun tagsColdAppLaunch() {
+        val tagTrace = tagsColdAppLaunch
+        Truth.assertWithMessage("Should have 2 app launch tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 677617096496959 // Represents 7d20h13m37s96ms
+        val endTagTimestamp = 677617685370716 // Represents 7d20h13m37s685ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsWarmAppLaunch() {
+        val tagTrace = tagsWarmAppLaunch
+        Truth.assertWithMessage("Should have 2 app launch tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 677630638463152 // Represents 7d20h13m50s638ms
+        val endTagTimestamp = 677631170881851 // Represents 7d20h13m51s170ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsAppLaunchByIntent() {
+        val tagTrace = tagsAppLaunchByIntent
+        Truth.assertWithMessage("Should have 2 app launch tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 678152269074856 // Represents 7d20h22m32s269ms
+        val endTagTimestamp = 678152921944244 // Represents 7d20h22m32s921ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsAppLaunchWithRotation() {
+        val tagTrace = tagsAppLaunchWithRotation
+        Truth.assertWithMessage("Should have 2 app launch tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 765902849663314 // Represents 8d20h45m2s849ms
+        val endTagTimestamp = 765903475287491 // Represents 8d20h45m4s139ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun doesNotTagComposeNewMessage() {
+        val tagTrace = tagsComposeNewMessage
+        Truth.assertWithMessage("Should have 0 app launch tags")
+            .that(tagTrace)
+            .isEmpty()
+    }
+
+    @Test
+    fun doesNotTagRotation() {
+        val tagTrace = tagsRotation
+        Truth.assertWithMessage("Should have 0 app launch tags")
+            .that(tagTrace)
+            .isEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt
new file mode 100644
index 0000000..827a7aa
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.ImeAppearProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [ImeAppearProcessor] tests. To run this test:
+ * `atest FlickerLibTest:ImeAppearProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ImeAppearProcessorTest {
+    private val processor = ImeAppearProcessor { }
+
+    private val tagsImeAppearWithGesture by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsImeAppearWithoutGestureOnAppLaunch by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val doesntTagStationaryIme by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsImeAppearHorizontal by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesImeTagsOnGesture() {
+        val tagTrace = tagsImeAppearWithGesture
+        Truth.assertWithMessage("Should have 2 Ime appear tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 11120377206699 // 0d3h5m20s377ms
+        val endTagTimestamp = 11120645554330 // 0d3h5m20s645ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesTagsOnAppLaunchWithNoGesture() {
+        val tagTrace = tagsImeAppearWithoutGestureOnAppLaunch
+        Truth.assertWithMessage("Should have 2 Ime appear tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 437392442580 // 0d0h7m17s392ms
+        val endTagTimestamp = 437396516487 // 0d0h7m17s396ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesTagsOnHorizontalAppLaunchWithGesture() {
+        val tagTrace = tagsImeAppearHorizontal
+        Truth.assertWithMessage("Should have 2 Ime appear tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 1015548115255262 // 11d18h5m48s115ms
+        val endTagTimestamp = 1015548233177878 // 11d18h5m48s223ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun doNotGenerateTagsOnStationaryIme() {
+        val tagTrace = doesntTagStationaryIme
+        Truth.assertWithMessage("Should have no Ime appear tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt
new file mode 100644
index 0000000..f5aae5a
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.ImeDisappearProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [ImeDisappearProcessor] tests. To run this test:
+ * `atest FlickerLibTest:ImeDisappearProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ImeDisappearProcessorTest {
+    private val processor = ImeDisappearProcessor { }
+
+    private val tagsImeDisappearWithGestureOpenAndClose by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsImeDisappearOnAppOpenAndClose by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsImeDisappearWithGestureClose by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsImeDisappearOnAppClose by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesImeDisappearTagsWithGestureOpenAndCloseIme() {
+        val tagTrace = tagsImeDisappearWithGestureOpenAndClose
+        Truth.assertWithMessage("Should have 2 IME disappear tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 69234720627579 // 19h13m54s720ms
+        val endTagTimestamp = 69234929459162 // 19h13m54s929ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesImeDisappearTagsOnAppOpenAndClose() {
+        val tagTrace = tagsImeDisappearOnAppOpenAndClose
+        Truth.assertWithMessage("Should have 2 IME disappear tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 69524600678331 // 19h18m44s600ms
+        val endTagTimestamp = 69524958584304 // 19h18m44s958ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesImeDisappearTagsWithGestureCloseIme() {
+        val tagTrace = tagsImeDisappearWithGestureClose
+        Truth.assertWithMessage("Should have 2 IME disappear tags")
+            .that(tagTrace)
+            .hasSize(2)
+
+        val startTagTimestamp = 69387450340971 // 19h16m27s450ms
+        val endTagTimestamp = 69387644316302 // 19h16m27s644ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesImeDisappearTagsOnAppClose() {
+        val tagTrace = tagsImeDisappearOnAppClose
+        Truth.assertWithMessage("Should have 2 IME disappear tags")
+            .that(tagTrace)
+            .hasSize(2)
+
+        val startTagTimestamp = 69635457764375 // 19h20m35s457ms
+        val endTagTimestamp = 69635765486645 // 19h20m35s765ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt
new file mode 100644
index 0000000..32eb1bb
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.PipEnterProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [PipEnterProcessor] tests. To run this test:
+ * `atest FlickerLibTest:PipEnterProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipEnterProcessorTest {
+    private val processor = PipEnterProcessor {}
+
+    private val tagsPipEnterWithoutRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsPipEnterWithRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsPipEnterWithSplitScreen by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsPipEnterTwice by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/twice/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsStationaryPip by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesPipEnterTagsWithoutRotation() {
+        val tagTrace = tagsPipEnterWithoutRotation
+        Truth.assertWithMessage("Should have 2 PIP enter tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 224969581940 // 3m44s969ms
+        val endTagTimestamp = 225315964162 // 3m45s315ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesPipEnterTagsWithRotation() {
+        val tagTrace = tagsPipEnterWithRotation
+        Truth.assertWithMessage("Should have 2 PIP enter tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 7546761373566 // 2h5m46s761ms
+        val endTagTimestamp = 7547420542538 // 2h5m47s420ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesPipEnterTagsWithSplitScreen() {
+        val tagTrace = tagsPipEnterWithSplitScreen
+        Truth.assertWithMessage("Should have 2 PIP enter tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 7706716317365 // 2h8m26s716ms
+        val endTagTimestamp = 7707029441615 // 2h8m27s29ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesPipEnterTagsTwice() {
+        val tagTrace = tagsPipEnterTwice
+        Truth.assertWithMessage("Should have 4 PIP enter tags")
+            .that(tagTrace)
+            .hasSize(4)
+        val firstPipEnter = arrayOf(126177655536, 126747555592) // 2m6s177ms, 2m6s747ms
+        val secondPipEnter = arrayOf(132780039058, 133367243856) // 2m12s780ms, 2m13s367ms
+        Truth.assertThat(tagTrace[0].timestamp).isEqualTo(firstPipEnter[0])
+        Truth.assertThat(tagTrace[1].timestamp).isEqualTo(firstPipEnter[1])
+        Truth.assertThat(tagTrace[2].timestamp).isEqualTo(secondPipEnter[0])
+        Truth.assertThat(tagTrace[3].timestamp).isEqualTo(secondPipEnter[1])
+    }
+
+    @Test
+    fun doesNotGeneratePipEnterTagsOnStationaryPip() {
+        val tagTrace = tagsStationaryPip
+        Truth.assertWithMessage("Should have no PIP enter tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt
new file mode 100644
index 0000000..a0d7038
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.PipExitProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [PipExitProcessor] tests. To run this test:
+ * `atest FlickerLibTest:PipExitProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipExitProcessorTest {
+    private val processor = PipExitProcessor { }
+
+    private val tagPipExitByDismissButton by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagPipExitBySwipe by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagPipExpand by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/expand/expand/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesPipExitTagsByDismissButton() {
+        val tagTrace = tagPipExitByDismissButton
+        Truth.assertWithMessage("Should have 2 pip exit tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 2852929744046 // 0d0h47m32s929ms
+        val endTagTimestamp = 2853783914340 // 0d0h47m33s783ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesPipExitTagsBySwipe() {
+        val tagTrace = tagPipExitBySwipe
+        Truth.assertWithMessage("Should have 2 pip exit tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 2057767033167 // 0d0h34m17s767ms
+        val endTagTimestamp = 2058963092661 // 0d0h34m18s963ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun doesNotTagPipExitOnPipExpand() {
+        val tagTrace = tagPipExpand
+        Truth.assertWithMessage("Should have 0 pip exit tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt
new file mode 100644
index 0000000..bca7f71
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.PipExpandProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [PipExpandProcessor] tests. To run this test:
+ * `atest FlickerLibTest:PipExpandProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipExpandProcessorTest {
+    private val processor = PipExpandProcessor { }
+
+    private val tagsPipExpanding by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/expand/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/expand/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsPipEnterTwice by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/enter/twice/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesPipExpandTags() {
+        val tagTrace = tagsPipExpanding
+        Truth.assertWithMessage("Should have 2 pip expand tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 2881856703699 // 0d0h48m1s856ms
+        val endTagTimestamp = 2882177502376 // 0d0h0m48m2s177ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun doesntTagPipEnterTwice() {
+        val tagTrace = tagsPipEnterTwice
+        Truth.assertWithMessage("Should have 0 pip expand tags")
+            .that(tagTrace)
+            .hasSize(0)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt
new file mode 100644
index 0000000..34a87df
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.PipResizeProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [PipResizeProcessor] tests. To run this test:
+ * `atest FlickerLibTest:PipResizeProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class PipResizeProcessorTest {
+    private val processor = PipResizeProcessor { }
+
+    private val tagsPipResizingToExpand by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/resize/expand/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsPipResizingToShrink by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope"
+        )
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope"
+        )
+        processor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun generatesPipResizeTagsOnExpand() {
+        val tagTrace = tagsPipResizingToExpand
+        Truth.assertWithMessage("Should have 2 pip resize tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 175188084434996 // 2d0h39m48s84ms
+        val endTagTimestamp = 175188414547217 // 2d0h39m48s414ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun generatesPipResizeTagsOnShrink() {
+        val tagTrace = tagsPipResizingToShrink
+        Truth.assertWithMessage("Should have 2 pip resize tags")
+            .that(tagTrace)
+            .hasSize(2)
+        val startTagTimestamp = 183718779433562 // 2d3h1m58s779ms
+        val endTagTimestamp = 183719115416407 // 2d3h1m59s115ms
+        Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt
new file mode 100644
index 0000000..0245a59
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.service.processors
+
+import com.android.server.wm.flicker.readLayerTraceFromFile
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.traces.common.service.processors.RotationProcessor
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [RotationProcessor] tests. To run this test:
+ * `atest FlickerLibTest:RotationProcessorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RotationProcessorTest {
+    companion object {
+        const val REGULAR_ROTATION1_START = 280186737540384
+        const val REGULAR_ROTATION1_END = 280187243649340
+        const val REGULAR_ROTATION2_START = 280188522078113
+        const val REGULAR_ROTATION2_END = 280189020672174
+        const val SEAMLESS_ROTATION_START = 981157456801L
+        const val SEAMLESS_ROTATION_END = 981560560070L
+        const val DISPLAYS_ROTATION1_START = 67585958089516
+        const val DISPLAYS_ROTATION1_END = 67586545923169
+        const val DISPLAYS_ROTATION2_START = 67587517164151
+        const val DISPLAYS_ROTATION2_END = 67588122219420
+    }
+
+    private val rotationProcessor = RotationProcessor { }
+    private val tagsRegularRotationTagFinalState by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/rotation/regular/WindowManagerTrace.winscope")
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope")
+        rotationProcessor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsSeamlessRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/rotation/seamless/WindowManagerTrace.winscope")
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope")
+        rotationProcessor.generateTags(wmTrace, layersTrace)
+    }
+
+    private val tagsDisplaysRotation by lazy {
+        val wmTrace = readWmTraceFromFile(
+            "tagprocessors/rotation/displays/WindowManagerTrace.winscope")
+        val layersTrace = readLayerTraceFromFile(
+            "tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope")
+        rotationProcessor.generateTags(wmTrace, layersTrace)
+    }
+
+    @Test
+    fun canDetectMultipleRegularRotations() {
+        Truth.assertWithMessage("Number of tags")
+            .that(tagsRegularRotationTagFinalState)
+            .hasSize(4)
+        val tags = tagsRegularRotationTagFinalState.flatMap { it.tags.toList() }
+        Truth.assertWithMessage("Number of start tags")
+            .that(tags.filter { it.isStartTag })
+            .hasSize(2)
+        Truth.assertWithMessage("Number of end tags")
+            .that(tags.filterNot { it.isStartTag })
+            .hasSize(2)
+    }
+
+    @Test
+    fun canDetectRegularRotation1Start() {
+        val tag = tagsRegularRotationTagFinalState
+            .firstOrNull { it.tags.any { tag -> tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("Start tag timestamp")
+            .that(tag)
+            .isEqualTo(REGULAR_ROTATION1_START)
+    }
+
+    @Test
+    fun canDetectRegularRotation1End() {
+        val tag = tagsRegularRotationTagFinalState
+            .firstOrNull { it.tags.any { tag -> !tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("End tag timestamp")
+            .that(tag)
+            .isEqualTo(REGULAR_ROTATION1_END)
+    }
+
+    @Test
+    fun canDetectRegularRotation2Start() {
+        val tag = tagsRegularRotationTagFinalState
+            .lastOrNull { it.tags.any { tag -> tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("Start tag timestamp")
+            .that(tag)
+            .isEqualTo(REGULAR_ROTATION2_START)
+    }
+
+    @Test
+    fun canDetectRegularRotation2End() {
+        val tag = tagsRegularRotationTagFinalState
+            .lastOrNull { it.tags.any { tag -> !tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("End tag timestamp")
+            .that(tag)
+            .isEqualTo(REGULAR_ROTATION2_END)
+    }
+
+    @Test
+    fun canDetectSeamlessRotation() {
+        Truth.assertWithMessage("Number of tags")
+            .that(tagsSeamlessRotation)
+            .hasSize(2)
+        val tags = tagsSeamlessRotation.flatMap { it.tags.toList() }
+        Truth.assertWithMessage("Number of start tags")
+            .that(tags.filter { it.isStartTag })
+            .hasSize(1)
+        Truth.assertWithMessage("Number of end  tags")
+            .that(tags.filterNot { it.isStartTag })
+            .hasSize(1)
+    }
+
+    @Test
+    fun canDetectSeamlessRotationStart() {
+        val tag = tagsSeamlessRotation
+            .lastOrNull { it.tags.any { tag -> tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("Start tag timestamp")
+            .that(tag)
+            .isEqualTo(SEAMLESS_ROTATION_START)
+    }
+
+    @Test
+    fun canDetectSeamlessRotationEnd() {
+        val tag = tagsSeamlessRotation
+            .lastOrNull { it.tags.any { tag -> !tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("End tag timestamp")
+            .that(tag)
+            .isEqualTo(SEAMLESS_ROTATION_END)
+    }
+
+    @Test
+    fun canDetectDisplaysRotation() {
+        Truth.assertWithMessage("Number of tags")
+            .that(tagsDisplaysRotation)
+            .hasSize(4)
+        val tags = tagsDisplaysRotation.flatMap { it.tags.toList() }
+        Truth.assertWithMessage("Number of start tags")
+            .that(tags.filter { it.isStartTag })
+            .hasSize(2)
+        Truth.assertWithMessage("Number of end  tags")
+            .that(tags.filterNot { it.isStartTag })
+            .hasSize(2)
+    }
+
+    @Test
+    fun canDetectDisplaysRotation1Start() {
+        val tag = tagsDisplaysRotation
+            .firstOrNull { it.tags.any { tag -> tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("Start tag timestamp")
+            .that(tag)
+            .isEqualTo(DISPLAYS_ROTATION1_START)
+    }
+
+    @Test
+    fun canDetectDisplaysRotation1End() {
+        val tag = tagsDisplaysRotation
+            .firstOrNull { it.tags.any { tag -> !tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("End tag timestamp")
+            .that(tag)
+            .isEqualTo(DISPLAYS_ROTATION1_END)
+    }
+
+    @Test
+    fun canDetectDisplaysRotation2Start() {
+        val tag = tagsDisplaysRotation
+            .lastOrNull { it.tags.any { tag -> tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("Start tag timestamp")
+            .that(tag)
+            .isEqualTo(DISPLAYS_ROTATION2_START)
+    }
+
+    @Test
+    fun canDetectDisplaysRotation2End() {
+        val tag = tagsDisplaysRotation
+            .lastOrNull { it.tags.any { tag -> !tag.isStartTag } }
+            ?.timestamp ?: 0L
+        Truth.assertWithMessage("End tag timestamp")
+            .that(tag)
+            .isEqualTo(DISPLAYS_ROTATION2_END)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateHelperTest.kt
similarity index 67%
rename from libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt
rename to libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateHelperTest.kt
index 4c18c68..dd6f5c2 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateHelperTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 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.
@@ -14,22 +14,27 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker
+package com.android.server.wm.flicker.windowmanager
 
-import android.content.ComponentName
 import android.view.Display
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.readWmTraceFromDumpFile
+import com.android.server.wm.flicker.readWmTraceFromFile
 import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
 import com.android.server.wm.traces.common.Buffer
 import com.android.server.wm.traces.common.Color
+import com.android.server.wm.traces.common.DeviceStateDump
+import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.RectF
 import com.android.server.wm.traces.common.Region
 import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
 import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder
 import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.google.common.truth.Truth
@@ -44,29 +49,28 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 class WindowManagerStateHelperTest {
     class TestWindowManagerStateHelper(
+        _wmState: WindowManagerState,
         /**
          * Predicate to supply a new UI information
          */
-        deviceDumpSupplier: () -> Dump,
+        deviceDumpSupplier: () -> DeviceStateDump<WindowManagerState, LayerTraceEntry>,
         numRetries: Int = 5,
         retryIntervalMs: Long = 500L
     ) : WindowManagerStateHelper(InstrumentationRegistry.getInstrumentation(),
         deviceDumpSupplier, numRetries, retryIntervalMs) {
-        var wmState = computeState(ignoreInvalidStates = true).wmState
-        override fun computeState(ignoreInvalidStates: Boolean): Dump {
-            val state = super.computeState(ignoreInvalidStates)
-            wmState = state.wmState
-            return state
+        var wmState: WindowManagerState = _wmState
+            private set
+
+        override fun updateCurrState(value: DeviceStateDump<WindowManagerState, LayerTraceEntry>) {
+            wmState = value.wmState
         }
     }
 
-    private fun String.toComponentName() =
-        ComponentName.unflattenFromString(this) ?: error("Unable to extract component name")
-
-    private val chromeComponentName = ("com.android.chrome/org.chromium.chrome.browser" +
-        ".firstrun.FirstRunActivity").toComponentName()
-    private val simpleAppComponentName = "com.android.server.wm.flicker.testapp/.SimpleActivity"
-        .toComponentName()
+    private val chromeComponent = FlickerComponentName.unflattenFromString(
+        "com.android.chrome/org.chromium.chrome.browser" +
+        ".firstrun.FirstRunActivity")
+    private val simpleAppComponentName = FlickerComponentName.unflattenFromString(
+        "com.android.server.wm.flicker.testapp/.SimpleActivity")
 
     private fun createImaginaryLayer(name: String, index: Int, id: Int, parentId: Int): Layer {
         val transform = Transform(0, Transform.Matrix(0f, 0f, 0f, 0f, 0f, 0f))
@@ -84,7 +88,7 @@
             visibleRegion = Region(rect.toRect()),
             activeBuffer = Buffer(1, 1, 1, 1),
             flags = 0,
-            _bounds = rect,
+            bounds = rect,
             color = Color(0f, 0f, 0f, 1f),
             _isOpaque = true,
             shadowRadius = 0f,
@@ -92,7 +96,7 @@
             type = "",
             _screenBounds = rect,
             transform = transform,
-            _sourceBounds = rect,
+            sourceBounds = rect,
             currFrame = 0,
             effectiveScalingMode = 0,
             bufferTransform = transform,
@@ -106,36 +110,35 @@
         )
     }
 
-    private fun createImaginaryVisibleLayers(names: List<String>): List<Layer> {
+    private fun createImaginaryVisibleLayers(names: List<FlickerComponentName>): Array<Layer> {
         val root = createImaginaryLayer("root", -1, id = "root".hashCode(), parentId = -1)
         val layers = mutableListOf(root)
         names.forEachIndexed { index, name ->
             layers.add(
-                createImaginaryLayer(name, index, id = name.hashCode(), parentId = root.id)
+                createImaginaryLayer(name.toLayerName(), index, id = name.hashCode(),
+                        parentId = root.id)
             )
         }
-        return layers
+        return layers.toTypedArray()
     }
 
     private fun WindowManagerTrace.asSupplier(
         startingTimestamp: Long = 0
-    ): () -> WindowManagerStateHelper.Dump {
+    ): () -> DeviceStateDump<WindowManagerState, LayerTraceEntry> {
         val iterator = this.dropWhile { it.timestamp < startingTimestamp }.iterator()
         return {
             if (iterator.hasNext()) {
                 val wmState = iterator.next()
-                val layerList = mutableListOf(WindowManagerStateHelper.STATUS_BAR_LAYER_NAME,
-                    WindowManagerStateHelper.NAV_BAR_LAYER_NAME)
+                val layerList = mutableListOf(FlickerComponentName.STATUS_BAR,
+                    FlickerComponentName.NAV_BAR)
 
                 if (wmState.inputMethodWindowState?.isSurfaceShown == true) {
-                    layerList.add(WindowManagerStateHelper.IME_LAYER_NAME)
+                    layerList.add(FlickerComponentName.IME)
                 }
                 val layerTraceEntry = LayerTraceEntryBuilder(timestamp = 0,
+                    displays = emptyArray(),
                     layers = createImaginaryVisibleLayers(layerList)).build()
-                WindowManagerStateHelper.Dump(
-                    wmState,
-                    layerTraceEntry
-                )
+                DeviceStateDump(wmState, layerTraceEntry)
             } else {
                 error("Reached the end of the trace")
             }
@@ -146,35 +149,37 @@
     fun canWaitForIme() {
         val trace = readWmTraceFromFile("wm_trace_ime.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         try {
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .isImeWindowVisible(Display.DEFAULT_DISPLAY)
+                .isNonAppWindowVisible(FlickerComponentName.IME)
             error("IME state should not be available")
         } catch (e: AssertionError) {
-            helper.waitImeWindowShown(Display.DEFAULT_DISPLAY)
+            helper.waitImeShown(Display.DEFAULT_DISPLAY)
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .isImeWindowVisible(Display.DEFAULT_DISPLAY)
+                .isNonAppWindowVisible(FlickerComponentName.IME)
         }
     }
 
     @Test
     fun canFailImeNotShown() {
-        val supplier = readWmTraceFromFile("wm_trace_ime.pb").asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, retryIntervalMs = 1)
+        val trace = readWmTraceFromFile("wm_trace_ime.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         try {
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .isImeWindowVisible()
+                .isNonAppWindowVisible(FlickerComponentName.IME)
             error("IME state should not be available")
         } catch (e: AssertionError) {
-            helper.waitImeWindowShown()
+            helper.waitImeShown()
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .isImeWindowInvisible()
+                .isNonAppWindowVisible(FlickerComponentName.IME)
         }
     }
 
@@ -182,18 +187,18 @@
     fun canWaitForWindow() {
         val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         try {
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .contains(simpleAppComponentName)
+                .containsAppWindow(simpleAppComponentName)
             error("Chrome window should not exist in the start of the trace")
         } catch (e: AssertionError) {
             helper.waitForVisibleWindow(simpleAppComponentName)
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .isVisible(simpleAppComponentName)
+                .isAppWindowVisible(simpleAppComponentName)
         }
     }
 
@@ -201,11 +206,12 @@
     fun canFailWindowNotShown() {
         val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = 3, retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = 3, retryIntervalMs = 1)
         try {
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
-                .contains(simpleAppComponentName)
+                .containsAppWindow(simpleAppComponentName)
             error("SimpleActivity window should not exist in the start of the trace")
         } catch (e: AssertionError) {
             helper.waitForVisibleWindow(simpleAppComponentName)
@@ -219,15 +225,15 @@
     fun canDetectHomeActivityVisibility() {
         val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
             .isHomeActivityVisible()
-        helper.waitForVisibleWindow(chromeComponentName)
+        helper.waitForVisibleWindow(chromeComponent)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
-            .isHomeActivityVisible(false)
+            .isHomeActivityInvisible()
         helper.waitForHomeActivityVisible()
         WindowManagerStateSubject
             .assertThat(helper.wmState)
@@ -238,29 +244,31 @@
     fun canWaitActivityRemoved() {
         val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
             .isHomeActivityVisible()
-            .notContains(chromeComponentName)
-        helper.waitForVisibleWindow(chromeComponentName)
+            .notContains(chromeComponent)
+        helper.waitForVisibleWindow(chromeComponent)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
-            .isVisible(chromeComponentName)
-        helper.waitForActivityRemoved(chromeComponentName)
+            .isAppWindowVisible(chromeComponent)
+        helper.waitForActivityRemoved(chromeComponent)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
-            .notContains(chromeComponentName)
+            .notContains(chromeComponent)
             .isHomeActivityVisible()
     }
 
     @Test
     fun canWaitAppStateIdle() {
         val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
-        val supplier = trace.asSupplier(startingTimestamp = 69443911868523)
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val initialTimestamp = 69443911868523
+        val supplier = trace.asSupplier(startingTimestamp = initialTimestamp)
+        val initialEntry = trace.getEntry(initialTimestamp)
+        val helper = TestWindowManagerStateHelper(initialEntry, supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         try {
             WindowManagerStateSubject
                 .assertThat(helper.wmState)
@@ -280,8 +288,8 @@
     fun canWaitForRotation() {
         val trace = readWmTraceFromFile("wm_trace_rotation.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
             .hasRotation(Surface.ROTATION_0)
@@ -296,26 +304,6 @@
     }
 
     @Test
-    fun canFailRotationNotReached() {
-        val trace = readWmTraceFromFile("wm_trace_rotation.pb")
-        val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
-        WindowManagerStateSubject
-            .assertThat(helper.wmState)
-            .hasRotation(Surface.ROTATION_0)
-        try {
-            helper.waitForRotation(Surface.ROTATION_90)
-            error("Should not have reached orientation ${Surface.ROTATION_90}")
-        } catch (e: IllegalStateException) {
-            WindowManagerStateSubject
-                .assertThat(helper.wmState)
-                .isNotRotation(Surface.ROTATION_90)
-                .hasRotation(Surface.ROTATION_0)
-        }
-    }
-
-    @Test
     fun canDetectResumedActivitiesInStacks() {
         val trace = readWmTraceFromDumpFile("wm_trace_resumed_activities_in_stack.pb")
         val entry = trace.first()
@@ -330,11 +318,11 @@
     fun canWaitForRecents() {
         val trace = readWmTraceFromFile("wm_trace_open_recents.pb")
         val supplier = trace.asSupplier()
-        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
-            retryIntervalMs = 1)
+        val helper = TestWindowManagerStateHelper(trace.first(), supplier,
+            numRetries = trace.entries.size, retryIntervalMs = 1)
         WindowManagerStateSubject
             .assertThat(helper.wmState)
-            .isRecentsActivityVisible(visible = false)
+            .isRecentsActivityInvisible()
         helper.waitForRecentsActivityVisible()
         WindowManagerStateSubject
             .assertThat(helper.wmState)
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt
new file mode 100644
index 0000000..0fe73db
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.windowmanager
+
+import android.graphics.Region
+import com.android.server.wm.flicker.CHROME_SPLASH_SCREEN_COMPONENT
+import com.android.server.wm.flicker.IMAGINARY_COMPONENT
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.PIP_DISMISS_COMPONENT
+import com.android.server.wm.flicker.SCREEN_DECOR_COMPONENT
+import com.android.server.wm.flicker.SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT
+import com.android.server.wm.flicker.SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT
+import com.android.server.wm.flicker.WALLPAPER_COMPONENT
+import com.android.server.wm.flicker.assertFailure
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.lang.AssertionError
+
+/**
+ * Contains [WindowManagerStateSubject] tests.
+ * To run this test: `atest FlickerLibTest:WindowManagerStateSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerStateSubjectTest {
+    private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
+    // Launcher is visible in fullscreen in the first frame of the trace
+    private val traceFirstFrameTimestamp = 9213763541297
+    // The first frame where the chrome splash screen is shown
+    private val traceFirstChromeFlashScreenTimestamp = 9215551505798
+    // The bounds of the display used to generate the trace [trace]
+    private val displayBounds = Region(0, 0, 1440, 2960)
+    // The region covered by the status bar in the trace
+    private val statusBarRegion = Region(0, 0, 1440, 171)
+
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(trace).first().frameRegion(IMAGINARY_COMPONENT)
+        }
+        Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className)
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains(FlickerSubject.ASSERTION_TAG)
+    }
+
+    @Test
+    fun canDetectAboveAppWindowVisibility_isVisible() {
+        assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+            .containsAboveAppWindow(FlickerComponentName.NAV_BAR)
+            .containsAboveAppWindow(SCREEN_DECOR_COMPONENT)
+            .containsAboveAppWindow(FlickerComponentName.STATUS_BAR)
+    }
+
+    @Test
+    fun canDetectAboveAppWindowVisibility_isInvisible() {
+        val subject = assertThat(trace).entry(traceFirstFrameTimestamp)
+        var failure = assertThrows(AssertionError::class.java) {
+            subject.containsAboveAppWindow(PIP_DISMISS_COMPONENT)
+                .isNonAppWindowVisible(PIP_DISMISS_COMPONENT)
+        }
+        assertFailure(failure).factValue("Is Invisible").contains("pip-dismiss-overlay")
+
+        failure = assertThrows(AssertionError::class.java) {
+            subject.containsAboveAppWindow(FlickerComponentName.NAV_BAR)
+                .isNonAppWindowInvisible(FlickerComponentName.NAV_BAR)
+        }
+        assertFailure(failure).factValue("Is Visible").contains("NavigationBar")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_exactSize() {
+        val entry = assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+
+        entry.frameRegion(FlickerComponentName.STATUS_BAR)
+                .coversAtLeast(statusBarRegion)
+        entry.frameRegion(LAUNCHER_COMPONENT)
+            .coversAtLeast(displayBounds)
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_smallerRegion() {
+        val entry = assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+        entry.frameRegion(FlickerComponentName.STATUS_BAR)
+                .coversAtLeast(Region(0, 0, 100, 100))
+        entry.frameRegion(LAUNCHER_COMPONENT)
+            .coversAtLeast(Region(0, 0, 100, 100))
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_largerRegion() {
+        val subject = assertThat(trace).entry(traceFirstFrameTimestamp)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(FlickerComponentName.STATUS_BAR)
+                    .coversAtLeast(Region(0, 0, 1441, 171))
+        }
+        assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(LAUNCHER_COMPONENT)
+                .coversAtLeast(Region(0, 0, 1440, 2961))
+        }
+        assertFailure(failure).factValue("Uncovered region")
+            .contains("SkRegion((0,2960,1440,2961))")
+    }
+
+    @Test
+    fun canDetectWindowCoversExactlyRegion_exactSize() {
+        val entry = assertThat(trace)
+                .entry(traceFirstFrameTimestamp)
+
+        entry.frameRegion(FlickerComponentName.STATUS_BAR)
+                .coversExactly(statusBarRegion)
+        entry.frameRegion(LAUNCHER_COMPONENT)
+                .coversExactly(displayBounds)
+    }
+
+    @Test
+    fun canDetectWindowCoversExactlyRegion_smallerRegion() {
+        val subject = assertThat(trace).entry(traceFirstFrameTimestamp)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(FlickerComponentName.STATUS_BAR)
+                    .coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+                .contains("SkRegion((100,0,1440,100)(0,100,1440,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(LAUNCHER_COMPONENT)
+                    .coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+                .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))")
+    }
+
+    @Test
+    fun canDetectWindowCoversExactlyRegion_largerRegion() {
+        val subject = assertThat(trace).entry(traceFirstFrameTimestamp)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(FlickerComponentName.STATUS_BAR)
+                    .coversAtLeast(Region(0, 0, 1441, 171))
+        }
+        assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(LAUNCHER_COMPONENT)
+                    .coversAtLeast(Region(0, 0, 1440, 2961))
+        }
+        assertFailure(failure).factValue("Uncovered region")
+                .contains("SkRegion((0,2960,1440,2961))")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_extactSize() {
+        val entry = assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+        entry.frameRegion(FlickerComponentName.STATUS_BAR)
+                .coversAtMost(statusBarRegion)
+        entry.frameRegion(LAUNCHER_COMPONENT)
+            .coversAtMost(displayBounds)
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_smallerRegion() {
+        val subject = assertThat(trace).entry(traceFirstFrameTimestamp)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(FlickerComponentName.STATUS_BAR)
+                    .coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+            .contains("SkRegion((100,0,1440,100)(0,100,1440,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion(LAUNCHER_COMPONENT)
+                .coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+            .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_largerRegion() {
+        val entry = assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+
+        entry.frameRegion(FlickerComponentName.STATUS_BAR)
+                .coversAtMost(Region(0, 0, 1441, 171))
+        entry.frameRegion(LAUNCHER_COMPONENT)
+            .coversAtMost(Region(0, 0, 1440, 2961))
+    }
+
+    @Test
+    fun canDetectBelowAppWindowVisibility() {
+        assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+            .containsNonAppWindow(WALLPAPER_COMPONENT)
+    }
+
+    @Test
+    fun canDetectAppWindowVisibility() {
+        assertThat(trace)
+            .entry(traceFirstFrameTimestamp)
+            .containsAppWindow(LAUNCHER_COMPONENT)
+
+        assertThat(trace)
+            .entry(traceFirstChromeFlashScreenTimestamp)
+            .containsAppWindow(CHROME_SPLASH_SCREEN_COMPONENT)
+    }
+
+    @Test
+    fun canDetectAppWindowVisibilitySubject() {
+        val trace = readWmTraceFromFile("wm_trace_launcher_visible_background.pb")
+        val firstEntry = assertThat(trace).first()
+        val appWindowNames = firstEntry.wmState.appWindows.map { it.name }
+        firstEntry.verify("has1AppWindow").that(appWindowNames).hasSize(3)
+        firstEntry.verify("has1AppWindow").that(appWindowNames)
+                .contains("com.android.server.wm.flicker.testapp/" +
+                        "com.android.server.wm.flicker.testapp.SimpleActivity")
+    }
+
+    @Test
+    fun canDetectLauncherVisibility() {
+        val trace = readWmTraceFromFile("wm_trace_launcher_visible_background.pb")
+        val subject = assertThat(trace)
+        val firstTrace = subject.first()
+        firstTrace.isAppWindowInvisible(LAUNCHER_COMPONENT)
+
+        // launcher is at the same time visible an invisible because it
+        // contains 2 windows with the exact same name
+        val lastTrace = subject.last()
+        lastTrace.isAppWindowInvisible(LAUNCHER_COMPONENT)
+
+        subject.isAppWindowNotOnTop(LAUNCHER_COMPONENT)
+            .isAppWindowInvisible(LAUNCHER_COMPONENT)
+            .then()
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+            .forAllEntries()
+
+        subject.isAppWindowInvisible(LAUNCHER_COMPONENT)
+            .forAllEntries()
+    }
+
+    @Test
+    fun canFailWithReasonForVisibilityChecks_windowNotFound() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(traceFirstFrameTimestamp)
+                .containsNonAppWindow(IMAGINARY_COMPONENT)
+        }
+        assertFailure(failure).hasMessageThat()
+            .contains(IMAGINARY_COMPONENT.packageName)
+    }
+
+    @Test
+    fun canFailWithReasonForVisibilityChecks_windowNotVisible() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(traceFirstFrameTimestamp)
+                .containsNonAppWindow(FlickerComponentName.IME)
+                .isNonAppWindowVisible(FlickerComponentName.IME)
+        }
+        assertFailure(failure).factValue("Is Invisible")
+            .contains(FlickerComponentName.IME.packageName)
+    }
+
+    @Test
+    fun canDetectAppZOrder() {
+        assertThat(trace)
+            .entry(traceFirstChromeFlashScreenTimestamp)
+            .containsAppWindow(LAUNCHER_COMPONENT)
+            .isAppWindowVisible(LAUNCHER_COMPONENT)
+            .isAboveWindow(CHROME_SPLASH_SCREEN_COMPONENT, LAUNCHER_COMPONENT)
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+    }
+
+    @Test
+    fun canFailWithReasonForZOrderChecks_windowNotOnTop() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(traceFirstChromeFlashScreenTimestamp)
+                .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT)
+        }
+        assertFailure(failure)
+            .factValue("Found")
+            .contains(LAUNCHER_COMPONENT.packageName)
+    }
+
+    @Test
+    fun canDetectActivityVisibility() {
+        val trace = readWmTraceFromFile("wm_trace_split_screen.pb")
+        val lastEntry = assertThat(trace).last()
+        lastEntry.isAppWindowVisible(SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT)
+        lastEntry.isAppWindowVisible(SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt
new file mode 100644
index 0000000..60632fa
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.windowmanager
+
+import com.android.server.wm.flicker.CHROME_COMPONENT
+import com.android.server.wm.flicker.CHROME_SPLASH_SCREEN_COMPONENT
+import com.android.server.wm.flicker.IMAGINARY_COMPONENT
+import com.android.server.wm.flicker.IME_ACTIVITY_COMPONENT
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
+import com.android.server.wm.flicker.SCREEN_DECOR_COMPONENT
+import com.android.server.wm.flicker.WALLPAPER_COMPONENT
+import com.android.server.wm.flicker.assertFailure
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [WindowManagerTraceSubject] tests. To run this test: `atest
+ * FlickerLibTest:WindowManagerTraceSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerTraceSubjectTest {
+    private val chromeTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
+    private val imeTrace by lazy { readWmTraceFromFile("wm_trace_ime.pb") }
+
+    @Test
+    fun testVisibleAppWindowForRange() {
+        assertThat(chromeTrace)
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .forRange(9213763541297L, 9215536878453L)
+
+        assertThat(chromeTrace)
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+            .isAppWindowInvisible(CHROME_SPLASH_SCREEN_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .then()
+            .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT)
+            .isAppWindowInvisible(LAUNCHER_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .then()
+            .isAppWindowOnTop(CHROME_COMPONENT)
+            .isAppWindowInvisible(LAUNCHER_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .forRange(9215551505798L, 9216093628925L)
+    }
+
+    @Test
+    fun testCanTransitionInAppWindow() {
+        assertThat(chromeTrace)
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .then()
+            .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .then()
+            .isAppWindowOnTop(CHROME_COMPONENT)
+            .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanDetectTransitionWithOptionalValue() {
+        val trace = readWmTraceFromFile("wm_trace_open_from_overview.pb")
+        val subject = assertThat(trace)
+        subject.isAppWindowOnTop(LAUNCHER_COMPONENT)
+                .then()
+                .isAppWindowOnTop(FlickerComponentName.SNAPSHOT)
+                .then()
+                .isAppWindowOnTop(CHROME_COMPONENT)
+    }
+
+    @Test
+    fun testCanTransitionInAppWindow_withOptional() {
+        assertThat(chromeTrace)
+                .isAppWindowOnTop(LAUNCHER_COMPONENT)
+                .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+                .then()
+                .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT)
+                .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+                .then()
+                .isAppWindowOnTop(CHROME_COMPONENT)
+                .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT)
+                .forAllEntries()
+    }
+
+    @Test
+    fun testCanInspectBeginning() {
+        assertThat(chromeTrace)
+            .first()
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+            .containsAboveAppWindow(SCREEN_DECOR_COMPONENT)
+    }
+
+    @Test
+    fun testCanInspectAppWindowOnTop() {
+        assertThat(chromeTrace)
+            .first()
+            .isAppWindowOnTop(LAUNCHER_COMPONENT)
+
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(chromeTrace)
+                .first()
+                .isAppWindowOnTop(IMAGINARY_COMPONENT)
+                .fail("Could not detect the top app window")
+        }
+        assertFailure(failure).hasMessageThat().contains("ImaginaryWindow")
+    }
+
+    @Test
+    fun testCanInspectEnd() {
+        assertThat(chromeTrace)
+            .last()
+            .isAppWindowOnTop(CHROME_COMPONENT)
+            .containsAboveAppWindow(SCREEN_DECOR_COMPONENT)
+    }
+
+    @Test
+    fun testCanTransitionNonAppWindow() {
+        assertThat(imeTrace)
+            .skipUntilFirstAssertion()
+            .isNonAppWindowInvisible(FlickerComponentName.IME)
+            .then()
+            .isNonAppWindowVisible(FlickerComponentName.IME)
+            .forAllEntries()
+    }
+
+    @Test(expected = AssertionError::class)
+    fun testCanDetectOverlappingWindows() {
+        assertThat(imeTrace)
+            .noWindowsOverlap(FlickerComponentName.IME, FlickerComponentName.NAV_BAR,
+                    IME_ACTIVITY_COMPONENT)
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanTransitionAboveAppWindow() {
+        assertThat(imeTrace)
+            .skipUntilFirstAssertion()
+            .isAboveAppWindowInvisible(FlickerComponentName.IME)
+            .then()
+            .isAboveAppWindowVisible(FlickerComponentName.IME)
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanTransitionBelowAppWindow() {
+        val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
+        assertThat(trace)
+            .skipUntilFirstAssertion()
+            .isBelowAppWindowVisible(WALLPAPER_COMPONENT)
+            .then()
+            .isBelowAppWindowInvisible(WALLPAPER_COMPONENT)
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanDetectVisibleWindowsMoreThanOneConsecutiveEntry() {
+        val trace = readWmTraceFromFile("wm_trace_valid_visible_windows.pb")
+        assertThat(trace).visibleWindowsShownMoreThanOneConsecutiveEntry().forAllEntries()
+    }
+
+    @Test
+    fun testCanAssertWindowStateSequence() {
+        val windowStates = assertThat(chromeTrace).windowStates(
+            "com.android.chrome/org.chromium.chrome.browser.firstrun.FirstRunActivity")
+        val visibilityChange = windowStates.zipWithNext { current, next ->
+            current.windowState?.isVisible != next.windowState?.isVisible
+        }
+
+        Truth.assertWithMessage("Visibility should have changed only 1x in the trace")
+            .that(visibilityChange.count { it })
+            .isEqualTo(1)
+    }
+
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(chromeTrace).isEmpty()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceTest.kt
similarity index 86%
rename from libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt
rename to libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceTest.kt
index 8383938..df9d882 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceTest.kt
@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker
+package com.android.server.wm.flicker.windowmanager
 
+import com.android.server.wm.flicker.readTestFile
+import com.android.server.wm.flicker.readWmTraceFromFile
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
@@ -42,7 +44,7 @@
         val firstEntry = trace.entries[0]
         assertThat(firstEntry.timestamp).isEqualTo(9213763541297L)
         assertThat(firstEntry.windowStates.size).isEqualTo(10)
-        assertThat(firstEntry.visibleWindows.size).isEqualTo(6)
+        assertThat(firstEntry.visibleWindows.size).isEqualTo(5)
         assertThat(trace.entries[trace.entries.size - 1].timestamp)
                 .isEqualTo(9216093628925L)
     }
@@ -61,7 +63,7 @@
         } catch (e: Exception) {
             throw RuntimeException(e)
         }
-        assertWithMessage("Unable to parse dump").that(trace.entries).hasSize(1)
+        assertWithMessage("Unable to parse dump").that(trace).hasSize(1)
     }
 
     /**
@@ -137,4 +139,21 @@
         assertThat(entry.getIsIncompleteReason())
             .contains("No resumed activities found")
     }
+
+    @Test
+    fun canFilter() {
+        val splitWmTrace = trace.filter(9215895891561, 9216093628925)
+
+        assertThat(splitWmTrace).isNotEmpty()
+
+        assertThat(splitWmTrace.entries.first().timestamp).isEqualTo(9215895891561)
+        assertThat(splitWmTrace.entries.last().timestamp).isEqualTo(9216093628925)
+    }
+
+    @Test
+    fun canFilter_wrongTimestamps() {
+        val splitWmTrace = trace.filter(71607477186189, 71607812120180)
+
+        assertThat(splitWmTrace).isEmpty()
+    }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt
new file mode 100644
index 0000000..b7ab535
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.server.wm.flicker.windowmanager
+
+import com.android.server.wm.flicker.IMAGINARY_COMPONENT
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.readWmTraceFromFile
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class WindowStateSubjectTest {
+    private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
+
+    @Test
+    fun exceptionContainsDebugInfoImaginary() {
+        val error = assertThrows(AssertionError::class.java) {
+            WindowManagerTraceSubject.assertThat(trace)
+                    .first()
+                    .windowState(IMAGINARY_COMPONENT.className)
+                    .exists()
+        }
+        Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className)
+        Truth.assertThat(error).hasMessageThat().contains("What?")
+        Truth.assertThat(error).hasMessageThat().contains("Where?")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Window title")
+    }
+
+    @Test
+    fun exceptionContainsDebugInfoConcrete() {
+        val error = assertThrows(AssertionError::class.java) {
+            WindowManagerTraceSubject.assertThat(trace)
+                    .first()
+                    .subjects
+                    .first()
+                    .doesNotExist()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("What?")
+        Truth.assertThat(error).hasMessageThat().contains("Where?")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace start")
+        Truth.assertThat(error).hasMessageThat().contains("Trace file")
+        Truth.assertThat(error).hasMessageThat().contains("Entry")
+    }
+}
\ No newline at end of file
diff --git a/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java b/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java
new file mode 100644
index 0000000..3b46815
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 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.platform.test.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/** A rule that generates debug artifact files for failed tests. */
+public class FailureWatcher extends TestWatcher {
+    private static final String TAG = "FailureWatcher";
+    private final UiDevice mDevice;
+
+    public FailureWatcher() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    @Override
+    protected void failed(Throwable e, Description description) {
+        onError(mDevice, description, e);
+    }
+
+    public static File diagFile(Description description, String prefix, String ext) {
+        return new File(
+                getInstrumentation().getTargetContext().getFilesDir(),
+                prefix
+                        + "-"
+                        + description.getTestClass().getSimpleName()
+                        + "."
+                        + description.getMethodName()
+                        + "."
+                        + ext);
+    }
+
+    public static void onError(UiDevice device, Description description, Throwable e) {
+        if (device == null) return;
+        final File sceenshot = diagFile(description, "TestScreenshot", "png");
+        final File hierarchy = diagFile(description, "Hierarchy", "zip");
+
+        // Dump window hierarchy
+        try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
+            out.putNextEntry(new ZipEntry("bugreport.txt"));
+            dumpStringCommand("dumpsys window windows", out);
+            dumpStringCommand("dumpsys package", out);
+            out.closeEntry();
+
+            out.putNextEntry(new ZipEntry("visible_windows.zip"));
+            dumpCommand("cmd window dump-visible-window-views", out);
+            out.closeEntry();
+        } catch (IOException ex) {
+        }
+
+        Log.e(
+                TAG,
+                "Failed test "
+                        + description.getMethodName()
+                        + ",\nscreenshot will be saved to "
+                        + sceenshot
+                        + ",\nUI dump at: "
+                        + hierarchy
+                        + " (use go/web-hv to open the dump file)",
+                e);
+        device.takeScreenshot(sceenshot);
+
+        // Dump accessibility hierarchy
+        try {
+            device.dumpWindowHierarchy(diagFile(description, "AccessibilityHierarchy", "uix"));
+        } catch (IOException ex) {
+            Log.e(TAG, "Failed to save accessibility hierarchy", ex);
+        }
+    }
+
+    private static void dumpStringCommand(String cmd, OutputStream out) throws IOException {
+        out.write(("\n\n" + cmd + "\n").getBytes());
+        dumpCommand(cmd, out);
+    }
+
+    private static void dumpCommand(String cmd, OutputStream out) throws IOException {
+        try (AutoCloseInputStream in =
+                new AutoCloseInputStream(
+                        getInstrumentation().getUiAutomation().executeShellCommand(cmd))) {
+            FileUtils.copy(in, out);
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java
index d7bad34..11fbca4 100644
--- a/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.os.RemoteException;
+import android.os.SystemClock;
 
 import org.junit.runner.Description;
 
@@ -36,11 +37,18 @@
             int currentOrientation = getContext().getResources().getConfiguration().orientation;
             if (currentOrientation != ORIENTATION_LANDSCAPE) { // ORIENTATION_PORTRAIT
                 getUiDevice().setOrientationLeft();
-                int rotatedOrientation = getContext().getResources().getConfiguration().orientation;
-                assertEquals(
-                        "Orientation should be landscape",
-                        ORIENTATION_LANDSCAPE,
-                        rotatedOrientation);
+                for (int i = 0; i != 100; ++i) {
+                    int rotatedOrientation =
+                            getContext().getResources().getConfiguration().orientation;
+                    if (rotatedOrientation == ORIENTATION_LANDSCAPE) break;
+                    if (i == 99) {
+                        assertEquals(
+                                "Orientation should be landscape",
+                                ORIENTATION_LANDSCAPE,
+                                rotatedOrientation);
+                    }
+                    SystemClock.sleep(100);
+                }
             }
         } catch (RemoteException e) {
             String message = "RemoteException when forcing landscape rotation on the device";
diff --git a/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java b/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java
new file mode 100644
index 0000000..703afa9
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IMapsHelper;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+
+/** This rule allows to execute CUJ while Maps in pip state. */
+public class MapsPipRule extends TestWatcher {
+
+    @VisibleForTesting static final String MAPS_SEARCH_ADDRESS = "maps-close-to-pip-address";
+    String mapsAddressOption = "Golden Gate Bridge";
+
+    @VisibleForTesting static final String MAPS_TIMEOUT = "maps-timeout";
+    long mapsTimeout = 2000;
+
+    private static HelperAccessor<IMapsHelper> sMapsHelper =
+            new HelperAccessor<>(IMapsHelper.class);
+
+    @Override
+    protected void starting(Description description) {
+        mapsAddressOption = getArguments().getString(MAPS_SEARCH_ADDRESS, "Golden Gate Bridge");
+        mapsTimeout = Long.valueOf(getArguments().getString(MAPS_TIMEOUT, "2000"));
+
+        sMapsHelper.get().open();
+        sMapsHelper.get().doSearch(mapsAddressOption);
+        sMapsHelper.get().getDirections();
+        sMapsHelper.get().startNavigation();
+        sMapsHelper.get().goToNavigatePip();
+        SystemClock.sleep(mapsTimeout);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        executeShellCommand(String.format("am force-stop %s", sMapsHelper.get().getPackage()));
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java b/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java
new file mode 100644
index 0000000..66884f5
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IGoogleCameraHelper2;
+import android.platform.helpers.IPhotosHelper;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+
+/** This rule allows to execute CUJ while new picures uploading in cloud. */
+public class PhotoUploadRule extends TestWatcher {
+
+    @VisibleForTesting static final String PHOTO_COUNT = "photo-count";
+    int photoCount = 5;
+
+    @VisibleForTesting static final String TAKE_PHOTO_DELAY = "take-photo-delay";
+    long takePhotoDelay = 1000;
+
+    @VisibleForTesting static final String PHOTO_TIMEOUT = "photo-timeout";
+    long photoTimeout = 10000;
+
+    @VisibleForTesting static final String UPLOAD_PHOTO = "upload-photo";
+    boolean uploadPhoto = true;
+
+    @VisibleForTesting static final String UPLOAD_VIDEO = "upload-video";
+    boolean uploadVideo = false;
+
+    @VisibleForTesting static final String CAPTURE_VIDEO_DURATION = "capture-video-duration";
+    long captureVideoDuration = 1200000;
+
+    private static HelperAccessor<IPhotosHelper> sPhotosHelper =
+            new HelperAccessor<>(IPhotosHelper.class);
+
+    private static HelperAccessor<IGoogleCameraHelper2> sGoogleCameraHelper =
+            new HelperAccessor<>(IGoogleCameraHelper2.class);
+
+    @Override
+    protected void starting(Description description) {
+        photoCount = Integer.valueOf(getArguments().getString(PHOTO_COUNT, String.valueOf(5)));
+        photoTimeout = Long.valueOf(getArguments().getString(PHOTO_TIMEOUT, String.valueOf(10000)));
+        takePhotoDelay =
+                Long.valueOf(getArguments().getString(TAKE_PHOTO_DELAY, String.valueOf(1000)));
+        captureVideoDuration =
+                Long.valueOf(
+                        getArguments().getString(CAPTURE_VIDEO_DURATION, String.valueOf(30000)));
+        uploadPhoto = Boolean.valueOf(getArguments().getString(UPLOAD_PHOTO, String.valueOf(true)));
+        uploadVideo =
+                Boolean.valueOf(getArguments().getString(UPLOAD_VIDEO, String.valueOf(false)));
+
+        sPhotosHelper.get().open();
+        sPhotosHelper.get().disableBackupMode();
+        sGoogleCameraHelper.get().open();
+        if (uploadPhoto) {
+            sGoogleCameraHelper.get().takeMultiplePhotos(photoCount, takePhotoDelay);
+            SystemClock.sleep(photoTimeout);
+        }
+        if (uploadVideo) {
+            sGoogleCameraHelper.get().clickVideoTab();
+            sGoogleCameraHelper.get().clickCameraVideoButton();
+            SystemClock.sleep(captureVideoDuration);
+            sGoogleCameraHelper.get().clickCameraVideoButton();
+        }
+        sPhotosHelper.get().open();
+        sPhotosHelper.get().enableBackupMode();
+        sPhotosHelper.get().verifyContentStartedUploading();
+        sPhotosHelper.get().exit();
+    }
+
+    @Override
+    protected void finished(Description description) {
+        sPhotosHelper.get().open();
+        sPhotosHelper.get().removeContent();
+        sPhotosHelper.get().disableBackupMode();
+        sPhotosHelper.get().exit();
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java
new file mode 100644
index 0000000..fb9d052
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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.platform.test.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Rule which captures a screen record for a test. After adding this rule to the test class, apply
+ * the annotation @ScreenRecord to individual tests
+ */
+public class ScreenRecordRule implements TestRule {
+
+    private static final String TAG = "ScreenRecordRule";
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        if (description.getAnnotation(ScreenRecord.class) == null) {
+            return base;
+        }
+
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                Instrumentation inst = getInstrumentation();
+                UiAutomation automation = inst.getUiAutomation();
+                UiDevice device = UiDevice.getInstance(inst);
+
+                File outputFile = FailureWatcher.diagFile(description, "ScreenRecord", "mp4");
+                device.executeShellCommand("killall screenrecord");
+                ParcelFileDescriptor output =
+                        automation.executeShellCommand("screenrecord " + outputFile);
+                String screenRecordPid = device.executeShellCommand("pidof screenrecord");
+                try {
+                    base.evaluate();
+                } finally {
+                    device.executeShellCommand("kill -INT " + screenRecordPid);
+                    Log.e(TAG, "Screenrecord captured at: " + outputFile);
+                    output.close();
+                }
+                // Delete the file if the test was successful.
+                automation.executeShellCommand("rm " + outputFile);
+            }
+        };
+    }
+
+    /** Interface to indicate that the test should capture screenrecord */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface ScreenRecord {}
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java b/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java
new file mode 100644
index 0000000..e872770
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IYouTubeHelper;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+
+/** This rule allows to execute CUJ while YouTube in pip state. */
+public class YouTubePipRule extends TestWatcher {
+
+    @VisibleForTesting static final String YOUTUBE_PLAYBACK_TIMEOUT = "youtube-playback-time";
+    long playbackTimeout = 2000;
+
+    @VisibleForTesting static final String VIDEO_NAME = "video-name";
+    String videoName = "test-one-hour-video";
+
+    private static HelperAccessor<IYouTubeHelper> sYouTubeHelper =
+            new HelperAccessor<>(IYouTubeHelper.class).withPrefix("YouTubeHelper");
+
+    @Override
+    protected void starting(Description description) {
+        playbackTimeout = Long.valueOf(getArguments().getString(YOUTUBE_PLAYBACK_TIMEOUT, "2000"));
+        videoName = getArguments().getString(VIDEO_NAME, "test-one-hour-video");
+
+        sYouTubeHelper.get().open();
+        sYouTubeHelper.get().goToYourVideos();
+        SystemClock.sleep(playbackTimeout);
+        sYouTubeHelper.get().playYourVideo(videoName);
+        SystemClock.sleep(playbackTimeout);
+        sYouTubeHelper.get().goToYouTubePip();
+        SystemClock.sleep(playbackTimeout);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        executeShellCommand(String.format("am force-stop %s", sYouTubeHelper.get().getPackage()));
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java
index 032e943..42a4ffb 100644
--- a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java
@@ -19,10 +19,8 @@
 import android.util.Log;
 
 import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject;
+import com.android.server.wm.traces.common.FlickerComponentName;
 import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace;
-import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper;
-
-import java.util.LinkedList;
 
 /**
  * Rule used for validating the common window manager trace based flicker assertions applicable
@@ -31,19 +29,23 @@
 public class WindowManagerFlickerRuleCommon extends WindowManagerFlickerRuleBase {
 
     private static final String TAG = WindowManagerFlickerRuleCommon.class.getSimpleName();
+    private static final FlickerComponentName NAV_BAR_COMPONENT =
+            new FlickerComponentName("", "NavigationBar0");
+    private static final FlickerComponentName STATUS_BAR_COMPONENT =
+            new FlickerComponentName("", "StatusBar");
 
     protected void validateWMFlickerConditions(WindowManagerTrace wmTrace) {
         // Verify that there’s an non-app window with names NavigationBar, StatusBar above
         // the app window and is visible in all winscope log entries.
         WindowManagerTraceSubject.assertThat(wmTrace)
-                .showsAboveAppWindow(WindowManagerStateHelper.NAV_BAR_WINDOW_NAME)
-                .showsAboveAppWindow(WindowManagerStateHelper.STATUS_BAR_WINDOW_NAME)
+                .isAboveAppWindowVisible(NAV_BAR_COMPONENT)
+                .isAboveAppWindowVisible(STATUS_BAR_COMPONENT)
                 .forAllEntries();
 
         // Verify that all visible windows are visible for more than one consecutive entry
         // in the log entries.
         WindowManagerTraceSubject.assertThat(wmTrace)
-                .visibleWindowsShownMoreThanOneConsecutiveEntry(new LinkedList<String>())
+                .visibleWindowsShownMoreThanOneConsecutiveEntry()
                 .forAllEntries();
         Log.v(TAG, "Successfully verified the window manager flicker conditions.");
     }
diff --git a/libraries/health/utils/src/android/platform/test/util/TestFilter.java b/libraries/health/utils/src/android/platform/test/util/TestFilter.java
new file mode 100644
index 0000000..8f7c257
--- /dev/null
+++ b/libraries/health/utils/src/android/platform/test/util/TestFilter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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.platform.test.util;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+
+/** Contains a method for filtering specific tests to run. Expects the format class#method. */
+public final class TestFilter {
+
+    private static final String LOG_TAG = TestFilter.class.getSimpleName();
+    @VisibleForTesting static final String OPTION_NAME = "filter-tests";
+
+    private TestFilter() {}
+
+    public static boolean isFilteredOrUnspecified(Bundle arguments, Description description) {
+        String testFilters = arguments.getString(OPTION_NAME);
+        // If the argument is unspecified, always return true.
+        if (testFilters == null) {
+            return true;
+        }
+
+        String displayName = description.getDisplayName();
+
+        // Test the display name against all filter arguments.
+        for (String testFilter : testFilters.split(",")) {
+            String[] filterComponents = testFilter.split("#");
+            if (filterComponents.length != 2) {
+                Log.e(
+                        LOG_TAG,
+                        String.format(
+                                "Invalid filter-tests instrumentation argument supplied, %s.",
+                                testFilter));
+                continue;
+            }
+
+            String displayNameFilter =
+                    String.format(
+                            "%s(%s)",
+                            // method
+                            filterComponents[1],
+                            // class
+                            filterComponents[0]);
+            if (displayNameFilter.equals(displayName)) {
+                return true;
+            }
+        }
+        // If the argument is specified and no matches, return false.
+        return false;
+    }
+}
diff --git a/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java b/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java
new file mode 100644
index 0000000..eb8c42d
--- /dev/null
+++ b/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.platform.test.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TestFilterTest {
+    private static final Description DESCRIPTION1 =
+            Description.createTestDescription("testClassA", "method1");
+    private static final Description DESCRIPTION2 =
+            Description.createTestDescription("testClassB", "method2");
+
+    @Test
+    public void testFilters_singleTest() {
+        assertThat(
+                        TestFilter.isFilteredOrUnspecified(
+                                buildArguments("testClassA#method1"), DESCRIPTION1))
+                .isTrue();
+    }
+
+    @Test
+    public void testFilters_multipleTests() {
+        Bundle arguments = buildArguments("testClassA#method1,testClassB#method2");
+        assertThat(TestFilter.isFilteredOrUnspecified(arguments, DESCRIPTION1)).isTrue();
+        assertThat(TestFilter.isFilteredOrUnspecified(arguments, DESCRIPTION2)).isTrue();
+    }
+
+    @Test
+    public void testFilters_allTestsWhenUnspecified() {
+        assertThat(TestFilter.isFilteredOrUnspecified(new Bundle(), DESCRIPTION1)).isTrue();
+        assertThat(TestFilter.isFilteredOrUnspecified(new Bundle(), DESCRIPTION2)).isTrue();
+    }
+
+    @Test
+    public void testDoesNotFilter_otherTest() {
+        assertThat(
+                        TestFilter.isFilteredOrUnspecified(
+                                buildArguments("testClassA#method1"), DESCRIPTION2))
+                .isFalse();
+    }
+
+    @Test
+    public void testDoesNotThrow_onBadArguments() {
+        // If the argument is explicitly null, then it's treated as unspecified.
+        assertThat(TestFilter.isFilteredOrUnspecified(buildArguments(null), DESCRIPTION1)).isTrue();
+        // If the argument is any other invalid input, then it just won't match.
+        assertThat(TestFilter.isFilteredOrUnspecified(buildArguments(",,,"), DESCRIPTION1))
+                .isFalse();
+        assertThat(TestFilter.isFilteredOrUnspecified(buildArguments("a#,#b"), DESCRIPTION1))
+                .isFalse();
+    }
+
+    private Bundle buildArguments(String testFilterArg) {
+        Bundle args = new Bundle();
+        args.putString(TestFilter.OPTION_NAME, testFilterArg);
+        return args;
+    }
+}
diff --git a/tests/automotive/functional/home/Android.bp b/tests/automotive/functional/home/Android.bp
index 19f5b9b..6659538 100644
--- a/tests/automotive/functional/home/Android.bp
+++ b/tests/automotive/functional/home/Android.bp
@@ -30,5 +30,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","general-tests"],
 }
diff --git a/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java b/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java
new file mode 100644
index 0000000..cc73ff2
--- /dev/null
+++ b/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 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.platform.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoHomeHelper;
+import android.platform.helpers.IAutoMediaHelper;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoTestMediaAppHelper;
+import android.platform.test.option.StringOption;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaTestAppTest {
+    private static final String MEDIA_APP = "media-app";
+    private static final String TEST_MEDIA_APP = "Test Media App";
+
+    @ClassRule
+    public static StringOption mMediaTestApp =
+            new StringOption(MEDIA_APP).setRequired(false);
+
+    private static HelperAccessor<IAutoMediaHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaHelper.class);
+    private static HelperAccessor<IAutoTestMediaAppHelper> sTestMediaAppHelper =
+            new HelperAccessor<>(IAutoTestMediaAppHelper.class);
+    private static HelperAccessor<IAutoHomeHelper> sAutoHomeHelper =
+            new HelperAccessor<>(IAutoHomeHelper.class);
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+        // Load songs on Test Media App
+        sAutoHomeHelper.get().openMediaWidget();
+        sMediaCenterHelper.get().openMediaAppMenuItems();
+        String mediaAppName = TEST_MEDIA_APP;
+        if (mMediaTestApp != null
+                && mMediaTestApp.get() != null && !mMediaTestApp.get().isEmpty()) {
+            mediaAppName = mMediaTestApp.get();
+        }
+        sMediaCenterHelper.get().openApp(mediaAppName);
+        assertTrue("Not a media app",
+                sMediaCenterHelper.get().getMediaAppTitle().equals(mediaAppName));
+        sMediaCenterHelper.get().openMediaAppSettingsPage();
+        sTestMediaAppHelper.get().loadMediaInLocalMediaTestApp();
+    }
+
+    @Test
+    public void testPlayPauseMedia() {
+        sMediaCenterHelper.get().playMedia();
+        assertTrue("Song not playing.", sMediaCenterHelper.get().isPlaying());
+        sMediaCenterHelper.get().pauseMedia();
+        assertFalse("Song not paused.", sMediaCenterHelper.get().isPlaying());
+    }
+
+    @Test
+    public void testNextTrack() {
+        String currentSong = sMediaCenterHelper.get().getMediaTrackName();
+        sMediaCenterHelper.get().clickNextTrack();
+        assertNotEquals(
+                "Song playing has not been changed",
+                currentSong,
+                sMediaCenterHelper.get().getMediaTrackName());
+    }
+}
diff --git a/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java b/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java
new file mode 100644
index 0000000..af58d40
--- /dev/null
+++ b/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.platform.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoHomeHelper;
+import android.platform.helpers.IAutoMediaHelper;
+import android.platform.test.option.StringOption;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class NoUserLoggedInTest {
+
+    @ClassRule
+    public static StringOption mMediaApp =
+            new StringOption("media-app").setRequired(false).setDefault("YouTube Music");
+    private HelperAccessor<IAutoHomeHelper> mAutoHomeHelper;
+    private HelperAccessor<IAutoMediaHelper> mMediaCenterHelper;
+
+    public NoUserLoggedInTest() throws Exception {
+        mMediaCenterHelper = new HelperAccessor<>(IAutoMediaHelper.class);
+        mAutoHomeHelper = new HelperAccessor<>(IAutoHomeHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Test
+    public void testNoUserLogInMessage() {
+        mAutoHomeHelper.get().openMediaWidget();
+        mMediaCenterHelper.get().openMediaAppMenuItems();
+        mMediaCenterHelper.get().openApp(mMediaApp.get());
+
+        assertTrue("Not a media app.",
+                mMediaCenterHelper.get().getMediaAppTitle().equals(mMediaApp.get()));
+
+        String noUserLoginMsg = mMediaCenterHelper.get().getMediaAppUserNotLoggedInErrorMessage();
+        assertTrue("Incorrect Sign in error message.",
+                noUserLoginMsg.equals("Please sign in to YouTube Music."));
+    }
+}
diff --git a/tests/automotive/functional/notifications/Android.bp b/tests/automotive/functional/notifications/Android.bp
index c962633..0fe19d4 100644
--- a/tests/automotive/functional/notifications/Android.bp
+++ b/tests/automotive/functional/notifications/Android.bp
@@ -29,5 +29,5 @@
         "hamcrest-library",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","general-tests"],
 }
diff --git a/tests/automotive/functional/settings/Android.bp b/tests/automotive/functional/settings/Android.bp
index 6f575c5..47322f5 100644
--- a/tests/automotive/functional/settings/Android.bp
+++ b/tests/automotive/functional/settings/Android.bp
@@ -30,5 +30,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","general-tests"],
 }
diff --git a/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java
index 054e8b4..1350af3 100644
--- a/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java
+++ b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java
@@ -33,7 +33,7 @@
 
     @Test
     public void testScrollDownAndUp() {
-        sHelper.get().scrollDownOnePage(500);
-        sHelper.get().scrollUpOnePage(500);
+        sHelper.get().scrollDownOnePage();
+        sHelper.get().scrollUpOnePage();
     }
 }
diff --git a/tests/functional/devicehealthchecks/assets/bug_map b/tests/functional/devicehealthchecks/assets/bug_map
index 526d520..62898e2 100644
--- a/tests/functional/devicehealthchecks/assets/bug_map
+++ b/tests/functional/devicehealthchecks/assets/bug_map
@@ -1,8 +1,8 @@
 <test_name> <regex.no.spaces> <only_bug_number>
 system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_phone 166183732
 system_app_anr com.google.android.apps.dreamliner/.dnd.DockConditionProviderService 166174264
+system_app_anr com.google.android.gms[\s\S]*executing\sservice\scom.google.android.gms/.nearby.sharing.SharingTileService 193719277
 system_app_anr act=android.hardware.usb.action.USB_STATE[\S\s]*cmp=com.google.android.projection.gearhead[\s\S]*ConnectivityEventHandlerImpl\$ConnectivityEventBroadcastReceiver 130956983
-system_app_anr com.google.android.euicc[\s\S]*executing\sservice\scom.google.android.euicc/com.android.euicc.service.EuiccServiceImpl 174479972
 system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_64_phone 166183732
 system_app_anr executing\sservice\scom.google.android.as/com.google.android.apps.miphone.aiai.echo.notificationintelligence.scheduler.impl.NotificationJobService 192300119
 system_app_anr executing\sservice\scom.google.android.as/com.google.android.apps.miphone.aiai.echo.scheduler.EchoJobService 192300119
@@ -16,7 +16,6 @@
 system_app_crash com.google.android.as.*\s.*\s.*\s.*\s*.*act=android.net.wifi.STATE_CHANGE 161559360
 system_app_crash com.google.android.googlequicksearchbox:search.*\s.*\s.*\s.*\s.*11.26.*\s.*\s.*\s.*\s.*\s\s.*ConcurrentModificationException.*\s.*\s.*\s.*apps.gsa.shared.util.debug.a.g.a 171971925
 system_app_crash v10804[\s\S]*com.google.android.apps.scone.wifiscorer.WifiScorerService[\s\S]*android.content.Intent.getAction 174277223
-system_app_crash Unable\sto\sget\sprovider\sandroidx.core.content.FileProvider[\S\s]+Couldn't\sfind\smeta-data\sfor\sprovider\swith\sauthority\scom.verizon.mips.services 166457582
 system_app_native_crash HwBinder*.*com.android.bluetooth 155074413
 system_app_native_crash ReferenceQueueD*.*com.google.android.apps.safetyhub 162103095
 system_app_native_crash Binder*.*com.google.android.apps.safetyhub 162104694
diff --git a/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java b/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java
new file mode 100644
index 0000000..3711d3c
--- /dev/null
+++ b/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 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.platform.test.scenario.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Identifies scenario that should be run only on large screen devices. Note that this annotation
+ * doesn't do any filtering for screen size, it's up to the test author to annotate the test
+ * correctly.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface LargeScreenOnly {}