Add AppLaunchProcessor with tests using Finite State Machines

Test: Please see the AppLaunchProcessor class with the auto generating
tags for start and end of app launch transition. It detects currently
the new activity being created, the animation starting on screen, and
the completion of the app launch. See AppLaunchProcessorTest for output.

Bug: 196116270
Change-Id: I7eebb39c43c8b5ecb2fb8b8d60a6d43eedad2ba9
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/TaggingEngine.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/TaggingEngine.kt
index dc6c4a8..98121ba 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/TaggingEngine.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/TaggingEngine.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker.service
 
 import android.util.Log
+import com.android.server.wm.flicker.service.processors.AppLaunchProcessor
 import com.android.server.wm.flicker.service.processors.ImeAppearProcessor
 import com.android.server.wm.flicker.service.processors.RotationProcessor
 import com.android.server.wm.traces.common.layers.LayersTrace
@@ -34,6 +35,7 @@
 class TaggingEngine(private val outputDir: Path, private val testTag: String) {
     private val transitions = listOf(
         // TODO: Keep adding new transition processors to invoke
+        AppLaunchProcessor(),
         ImeAppearProcessor(),
         RotationProcessor()
     )
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessor.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessor.kt
index 2286c1c..7e12e8f 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessor.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessor.kt
@@ -16,39 +16,161 @@
 
 package com.android.server.wm.flicker.service.processors
 
-import com.android.server.wm.flicker.service.ITagProcessor
-import com.android.server.wm.traces.common.layers.LayersTrace
+import android.util.Log
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING
 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.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.WindowState
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerConditionsFactory.isAppLaunchEnded
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
-class AppLaunchProcessor : ITagProcessor {
-    override fun generateTags(wmTrace: WindowManagerTrace, layersTrace: LayersTrace): TagTrace {
-        // TODO(b/196116270): Remove the mock data and add the implementation for App Launch
-        return TagTrace(arrayOf(TagState(wmTrace.entries[0].timestamp,
-            arrayOf(
-                Tag(
-                    id = 1,
-                    transition = Transition.APP_LAUNCH,
-                    isStartTag = true,
-                    taskId = 2,
-                    windowToken = "",
-                    layerId = -1
-                )
-            )
-        ), TagState(wmTrace.entries[1].timestamp,
-            arrayOf(
-                Tag(
-                    id = 1,
-                    transition = Transition.APP_LAUNCH,
-                    isStartTag = false,
-                    taskId = 2,
-                    windowToken = "",
-                    layerId = -1
-                )
-            )
-        )), "")
+class AppLaunchProcessor : TransitionProcessor() {
+    override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) =
+            InitialState(tags)
+
+    /**
+     * Base state for the FSM, check if there are more WM and SF states to process
+     */
+    abstract class BaseState(tags: MutableMap<Long, MutableList<Tag>>) : FSMState(tags) {
+        protected abstract fun doProcessState(
+            previous: WindowManagerStateHelper.Dump?,
+            current: WindowManagerStateHelper.Dump,
+            next: WindowManagerStateHelper.Dump
+        ): FSMState
+
+        override fun process(
+            previous: WindowManagerStateHelper.Dump?,
+            current: WindowManagerStateHelper.Dump,
+            next: WindowManagerStateHelper.Dump?
+        ): FSMState? {
+            return when (next) {
+                null -> {
+                    // last state
+                    Log.v(LOG_TAG, "(${current.layerState.timestamp}) Trace has reached the end")
+                    if (hasOpenTag()) {
+                        Log.v(LOG_TAG, "(${current.layerState.timestamp}) Has an open tag, " +
+                                "closing it on the last SF state")
+                        addEndTransitionTag(current, Transition.IME_APPEAR)
+                    }
+                    null
+                }
+                else -> doProcessState(previous, current, next)
+            }
+        }
+    }
+
+    /**
+     * Initial FSM state that passes the current app launch activity if any to the next state.
+     */
+    class InitialState(
+        tags: MutableMap<Long, MutableList<Tag>>
+    ) : BaseState(tags) {
+        private val processor = AppLaunchProcessor()
+
+        override fun doProcessState(
+            previous: WindowManagerStateHelper.Dump?,
+            current: WindowManagerStateHelper.Dump,
+            next: WindowManagerStateHelper.Dump
+        ): FSMState {
+            val prevTaskActivities = processor.filterVisibleAppStartActivities(current.wmState)
+            val prevAppLaunchActivity = processor.appLaunchActivityWithSurface(prevTaskActivities)
+
+            return WaitNewAppLaunchActivity(tags, prevAppLaunchActivity)
+        }
+    }
+
+    /**
+     * Finds the app launch when a new [WindowManagerState] contains an activity that is resuming or
+     * initializing, with a SplashScreen Window that has type [TYPE_APPLICATION_STARTING] and
+     * window's surface is showing. This condition should not be true in the previous timestamp.
+     */
+    class WaitNewAppLaunchActivity(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val prevAppLaunchActivity: Activity?
+    ) : BaseState(tags) {
+        private val processor = AppLaunchProcessor()
+
+        override fun doProcessState(
+            previous: WindowManagerStateHelper.Dump?,
+            current: WindowManagerStateHelper.Dump,
+            next: WindowManagerStateHelper.Dump
+        ): FSMState {
+            if (previous == null) return this
+
+            /**
+             * Activities that have started and surfaced that were not already doing so in the
+             * previous timestamp.
+             */
+            val currTaskActivities = processor.filterVisibleAppStartActivities(current.wmState)
+            val currAppLaunchActivity = processor.appLaunchActivityWithSurface(currTaskActivities)
+
+            val startingActivityName = if (currAppLaunchActivity != null &&
+                    currAppLaunchActivity != prevAppLaunchActivity) {
+                currAppLaunchActivity.name
+            } else ""
+
+            return if (startingActivityName.isNotEmpty()) {
+                val taskId = current.wmState.rootTasks.first {
+                    it.containsActivity(startingActivityName)
+                }.taskId
+                Log.v(Transition.APP_LAUNCH.name,
+                        "(${current.wmState.timestamp}) Task $taskId appears to have launched")
+                addStartTransitionTag(current, Transition.IME_APPEAR, taskId = taskId)
+                WaitAppLaunchEnded(tags, taskId)
+            } else {
+                Log.v(Transition.APP_LAUNCH.name,
+                        "(${current.wmState.timestamp}) No Start of App Launch Detected")
+                WaitNewAppLaunchActivity(tags, currAppLaunchActivity)
+            }
+        }
+    }
+
+    fun filterVisibleAppStartActivities(
+        wmState: WindowManagerState
+    ): List<Activity> {
+        return wmState.rootTasks.flatMap { task ->
+            task.activities.filter {
+                (it.state == "RESUMED" || it.state == "INITIALIZING") && it.isVisible
+            }
+        }
+    }
+
+    fun appLaunchActivityWithSurface(
+        activities: List<Activity>
+    ): Activity? {
+        return activities.firstOrNull { activity ->
+            activity.children.filterIsInstance<WindowState>().any { window ->
+                window.attributes.type == TYPE_APPLICATION_STARTING && window.isSurfaceShown
+            }
+        }
+    }
+
+    /**
+     * Wait for SplashScreen window under the app task to no longer be visible as the splash screen
+     * has finished its job.
+     */
+    class WaitAppLaunchEnded(
+        tags: MutableMap<Long, MutableList<Tag>>,
+        private val taskId: Int
+    ) : BaseState(tags) {
+        override fun doProcessState(
+            previous: WindowManagerStateHelper.Dump?,
+            current: WindowManagerStateHelper.Dump,
+            next: WindowManagerStateHelper.Dump
+        ): FSMState {
+            val timestamp = current.wmState.timestamp
+
+            return if (isAppLaunchEnded(taskId).isSatisfied(current)) {
+                Log.v(Transition.APP_LAUNCH.name,
+                        "($timestamp) App has finished launching with task $taskId")
+                addEndTransitionTag(current, Transition.APP_LAUNCH, taskId = taskId)
+                InitialState(tags)
+            } else {
+                Log.v(Transition.APP_LAUNCH.name, "($timestamp) No end of app launch detected")
+                this
+            }
+        }
     }
 }
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
index e6a56df..1c17405 100644
--- 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
@@ -79,6 +79,12 @@
 
     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) }
     }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerConditionsFactory.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerConditionsFactory.kt
index 3b057e1..6bf53b3 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerConditionsFactory.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerConditionsFactory.kt
@@ -18,10 +18,13 @@
 
 import android.content.ComponentName
 import android.view.Display
+import android.view.WindowManager
 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.Companion.isFlagSet
 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.Condition
 import com.android.server.wm.traces.parser.ConditionList
 import com.android.server.wm.traces.parser.toActivityName
@@ -231,4 +234,15 @@
         Condition("isImeSurfaceShown") {
             it.wmState.inputMethodWindowState?.isSurfaceShown == true
         }
+
+    fun isAppLaunchEnded(taskId: Int): Condition<WindowManagerStateHelper.Dump> =
+        Condition("containsVisibleAppLaunchWindow[$taskId]") { dump ->
+            val windowStates = dump.wmState.getRootTask(taskId)?.activities?.flatMap {
+                it.children.filterIsInstance<WindowState>()
+            }
+            windowStates != null && windowStates.none { window ->
+                window.attributes.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING &&
+                        window.isVisible
+            }
+        }
 }
\ No newline at end of file
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..f74d59e
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/WindowManagerTrace.winscope
new file mode 100644
index 0000000..9742783
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/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..c04eb82
--- /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..e9e68b6
--- /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..cb69eb1
--- /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..b47ab85
--- /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..d5fecf7
--- /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..fd239ef
--- /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..7755c83
--- /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..118b26b
--- /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/pipresize/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pipresize/SurfaceFlingerTrace.winscope
new file mode 100644
index 0000000..3d8b511
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pipresize/SurfaceFlingerTrace.winscope
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pipresize/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pipresize/WindowManagerTrace.winscope
new file mode 100644
index 0000000..b4880bf
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/tagprocessors/pipresize/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/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..b969510
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.google.common.truth.Truth
+import org.junit.Test
+
+/**
+ * Contains [AppLaunchProcessor] tests. To run this test: `atest
+ * FlickerLibTest:AppLaunchProcessorTest`
+ */
+class AppLaunchProcessorTest {
+    /**
+     * Scenarios expecting tags
+     */
+    private val wmTraceColdAppLaunch =
+            readWmTraceFromFile(
+                    "tagprocessors/applaunch/cold/WindowManagerTrace.winscope"
+            )
+    private val sfTraceColdAppLaunch =
+            readLayerTraceFromFile(
+                    "tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope"
+            )
+    private val wmTraceWarmAppLaunch =
+            readWmTraceFromFile(
+                    "tagprocessors/applaunch/warm/WindowManagerTrace.winscope"
+            )
+    private val sfTraceWarmAppLaunch =
+            readLayerTraceFromFile(
+                    "tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope"
+            )
+    private val wmTraceAppLaunchByIntent =
+            readWmTraceFromFile(
+                    "tagprocessors/applaunch/intent/WindowManagerTrace.winscope"
+            )
+    private val sfTraceAppLaunchByIntent =
+            readLayerTraceFromFile(
+                    "tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope"
+            )
+    private val wmTraceAppLaunchWithRotation =
+            readWmTraceFromFile(
+                    "tagprocessors/applaunch/withrot/WindowManagerTrace.winscope"
+            )
+    private val sfTraceAppLaunchWithRotation =
+            readLayerTraceFromFile(
+                    "tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope"
+            )
+
+    /**
+     * Scenarios expecting no tags
+     */
+    private val wmTraceComposeNewMessage =
+            readWmTraceFromFile(
+                    "tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope"
+            )
+    private val sfTraceComposeNewMessage =
+            readLayerTraceFromFile(
+                    "tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope"
+            )
+    private val wmTraceRotation =
+            readWmTraceFromFile(
+                    "tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope"
+            )
+    private val sfTraceRotation =
+            readLayerTraceFromFile(
+                    "tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope"
+            )
+
+    @Test
+    fun tagsColdAppLaunch() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceColdAppLaunch, sfTraceColdAppLaunch).entries
+        Truth.assertThat(tagStates.size).isEqualTo(2)
+
+        val startTagTimestamp = 268309543090536 // Represents 3d2h31m49s543ms
+        val endTagTimestamp = 268310230837688 // Represents 3d2h31m50s230ms
+        Truth.assertThat(tagStates.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagStates.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsWarmAppLaunch() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceWarmAppLaunch, sfTraceWarmAppLaunch).entries
+        Truth.assertThat(tagStates.size).isEqualTo(2)
+
+        val startTagTimestamp = 237300088466617 // Represents 2d17h55m0s88ms
+        val endTagTimestamp = 237300592571094 // Represents 2d17h55m0s592ms
+        Truth.assertThat(tagStates.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagStates.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsAppLaunchByIntent() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceAppLaunchByIntent, sfTraceAppLaunchByIntent).entries
+        Truth.assertThat(tagStates.size).isEqualTo(2)
+
+        val startTagTimestamp = 80872063113909 // Represents 0d22h27m52s63ms
+        val endTagTimestamp = 80872910948056 // Represents 0d22h27m52s910ms
+        Truth.assertThat(tagStates.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagStates.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun tagsAppLaunchWithRotation() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceAppLaunchWithRotation, sfTraceAppLaunchWithRotation).entries
+        Truth.assertThat(tagStates.size).isEqualTo(2)
+
+        val startTagTimestamp = 17864904019309 // Represents 0d4h57m44s904ms
+        val endTagTimestamp = 17865482335252 // Represents 0d4h57m45s482ms
+        Truth.assertThat(tagStates.first().timestamp).isEqualTo(startTagTimestamp)
+        Truth.assertThat(tagStates.last().timestamp).isEqualTo(endTagTimestamp)
+    }
+
+    @Test
+    fun doesNotTagComposeNewMessage() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceComposeNewMessage, sfTraceComposeNewMessage).entries
+        Truth.assertThat(tagStates).isEmpty()
+    }
+
+    @Test
+    fun doesNotTagRotation() {
+        val tagStates = AppLaunchProcessor()
+                .generateTags(wmTraceRotation, sfTraceRotation).entries
+        Truth.assertThat(tagStates).isEmpty()
+    }
+}
\ No newline at end of file