Merge "Enable "auto rotate" for resizable AVD" into sc-v2-dev
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index 2f77f41..bf23983 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -18,6 +18,7 @@
     "typescript": "^4.3.5",
     "vue": "^2.6.14",
     "vue-context": "^6.0.0",
+    "vue-gtag": "^1.16.1",
     "vue-material": "^1.0.0-beta-15",
     "vuex": "^3.6.2"
   },
diff --git a/tools/winscope/spec/TagErrorSpec.js b/tools/winscope/spec/TagErrorSpec.js
index e8919a0..59cc138 100644
--- a/tools/winscope/spec/TagErrorSpec.js
+++ b/tools/winscope/spec/TagErrorSpec.js
@@ -1,6 +1,7 @@
 import { decodeAndTransformProto, FILE_TYPES, FILE_DECODERS } from '../src/decode';
 import Tag from '../src/flickerlib/tags/Tag';
 import Error from '../src/flickerlib/errors/Error';
+import { TaggingEngine } from '../src/flickerlib/common.js';
 import fs from 'fs';
 import path from 'path';
 
@@ -33,6 +34,26 @@
   })
 });
 
+describe("Detect Tag", () => {
+  it("can detect tags", () => {
+    const wmFile = '../spec/traces/regular_rotation_in_last_state_wm_trace.winscope'
+    const layersFile = '../spec/traces/regular_rotation_in_last_state_layers_trace.winscope'
+    const wmBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, wmFile)));
+    const layersBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, layersFile)));
+
+    const wmTrace = decodeAndTransformProto(wmBuffer, FILE_DECODERS[FILE_TYPES.WINDOW_MANAGER_TRACE].decoderParams, true);
+    const layersTrace = decodeAndTransformProto(layersBuffer, FILE_DECODERS[FILE_TYPES.SURFACE_FLINGER_TRACE].decoderParams, true);
+
+    const engine = new TaggingEngine(wmTrace, layersTrace, (text) => { console.log(text) });
+    const tagTrace = engine.run();
+    expect(tagTrace.size).toEqual(4);
+    expect(tagTrace.entries[0].timestamp.toString()).toEqual('280186737540384');
+    expect(tagTrace.entries[1].timestamp.toString()).toEqual('280187243649340');
+    expect(tagTrace.entries[2].timestamp.toString()).toEqual('280188522078113');
+    expect(tagTrace.entries[3].timestamp.toString()).toEqual('280189020672174');
+  })
+});
+
 describe("Error Transformation", () => {
   it("can transform error traces", () => {
     const buffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, errorTrace)));
@@ -47,4 +68,4 @@
     expect(data.entries[1].errors).toEqual([new Error("","",66,"",66)]);
     expect(data.entries[2].errors).toEqual([new Error("","",99,"",99)]);
   })
-});
\ No newline at end of file
+});
diff --git a/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope b/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope
new file mode 100644
index 0000000..36fc663
--- /dev/null
+++ b/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope
Binary files differ
diff --git a/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope b/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope
new file mode 100644
index 0000000..b5537ac
--- /dev/null
+++ b/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope
Binary files differ
diff --git a/tools/winscope/src/App.vue b/tools/winscope/src/App.vue
index d771761..2621066 100644
--- a/tools/winscope/src/App.vue
+++ b/tools/winscope/src/App.vue
@@ -19,6 +19,11 @@
         <h1 class="md-title" style="flex: 1">{{title}}</h1>
         <md-button
           class="md-primary md-theme-default download-all-btn"
+          @click="generateTags()"
+          v-if="dataLoaded && canGenerateTags"
+        >Generate Tags</md-button>
+        <md-button
+          class="md-primary md-theme-default"
           @click="downloadAsZip(files)"
           v-if="dataLoaded"
         >Download All</md-button>
@@ -62,7 +67,6 @@
           <overlay
             :presentTags="Object.freeze(presentTags)"
             :presentErrors="Object.freeze(presentErrors)"
-            :tagAndErrorTraces="tagAndErrorTraces"
             :store="store"
             :ref="overlayRef"
             :searchTypes="searchTypes"
@@ -86,6 +90,8 @@
 import {DIRECTION} from './utils/utils';
 import Searchbar from './Searchbar.vue';
 import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
+import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js';
+import { TaggingEngine } from './flickerlib/common';
 
 const APP_NAME = 'Winscope';
 
@@ -107,15 +113,17 @@
         navigationStyle: NAVIGATION_STYLE.GLOBAL,
         flickerTraceView: false,
         showFileTypes: [],
+        isInputMode: false,
       }),
       overlayRef: 'overlay',
       mainContentStyle: {
         'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`,
       },
+      tagFile: null,
       presentTags: [],
       presentErrors: [],
       searchTypes: [SEARCH_TYPE.TIMESTAMP],
-      tagAndErrorTraces: false,
+      hasTagOrErrorTraces: false,
     };
   },
   created() {
@@ -139,11 +147,14 @@
     },
     /** Get tags from all uploaded tag files*/
     getUpdatedTags() {
-      var tagStates = this.getUpdatedStates(this.tagFiles);
+      if (this.tagFile === null) return [];
+      const tagStates = this.getUpdatedStates([this.tagFile]);
       var tags = [];
       tagStates.forEach(tagState => {
         tagState.tags.forEach(tag => {
-          tag.timestamp = tagState.timestamp;
+          tag.timestamp = Number(tagState.timestamp);
+          // tags generated on frontend have transition.name due to kotlin enum
+          tag.transition = tag.transition.name ?? tag.transition;
           tags.push(tag);
         });
       });
@@ -156,20 +167,31 @@
       //TODO (b/196201487) add check if errors empty
       errorStates.forEach(errorState => {
         errorState.errors.forEach(error => {
-          error.timestamp = errorState.timestamp;
+          error.timestamp = Number(errorState.timestamp);
           errors.push(error);
         });
       });
       return errors;
     },
     /** Set flicker mode check for if there are tag/error traces uploaded*/
-    shouldUpdateTagAndErrorTraces() {
-      return this.tagFiles.length > 0 || this.errorFiles.length > 0;
+    updateHasTagOrErrorTraces() {
+      return this.hasTagTrace() || this.hasErrorTrace();
+    },
+    hasTagTrace() {
+      return this.tagFile !== null;
+    },
+    hasErrorTrace() {
+      return this.errorFiles.length > 0;
     },
     /** Activate flicker search tab if tags/errors uploaded*/
     updateSearchTypes() {
       this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
-      if (this.tagAndErrorTraces) this.searchTypes.push(SEARCH_TYPE.TAG);
+      if (this.hasTagTrace()) {
+        this.searchTypes.push(SEARCH_TYPE.TRANSITIONS);
+      }
+      if (this.hasErrorTrace()) {
+        this.searchTypes.push(SEARCH_TYPE.ERRORS);
+      }
     },
     /** Filter data view files by current show settings*/
     updateShowFileTypes() {
@@ -179,7 +201,9 @@
     },
     clear() {
       this.store.showFileTypes = [];
+      this.tagFile = null;
       this.$store.commit('clearFiles');
+      this.buttonClicked("Clear")
     },
     onDataViewFocus(file) {
       this.$store.commit('setActiveFile', file);
@@ -187,6 +211,7 @@
     },
     onKeyDown(event) {
       event = event || window.event;
+      if (this.store.isInputMode) return false;
       if (event.keyCode == 37 /* left */ ) {
         this.$store.dispatch('advanceTimeline', DIRECTION.BACKWARD);
       } else if (event.keyCode == 39 /* right */ ) {
@@ -203,7 +228,9 @@
     },
     onDataReady(files) {
       this.$store.dispatch('setFiles', files);
-      this.tagAndErrorTraces = this.shouldUpdateTagAndErrorTraces();
+
+      this.tagFile = this.tagFiles[0] ?? null;
+      this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
       this.presentTags = this.getUpdatedTags();
       this.presentErrors = this.getUpdatedErrors();
       this.updateSearchTypes();
@@ -224,11 +251,43 @@
           `${ CONTENT_BOTTOM_PADDING + newHeight }px`,
       );
     },
+    generateTags() {
+      // generate tag file
+      this.buttonClicked("Generate Tags");
+      const engine = new TaggingEngine(
+        this.$store.getters.tagGenerationWmTrace,
+        this.$store.getters.tagGenerationSfTrace,
+        (text) => { console.log(text) }
+      );
+      const tagTrace = engine.run();
+      const tagFile = this.generateTagFile(tagTrace);
+
+      // update tag trace in set files, update flicker mode
+      this.tagFile = tagFile;
+      this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
+      this.presentTags = this.getUpdatedTags();
+      this.presentErrors = this.getUpdatedErrors();
+      this.updateSearchTypes();
+    },
+
+    generateTagFile(tagTrace) {
+      const data = tagTrace.entries;
+      const blobUrl = URL.createObjectURL(new Blob([], {type: undefined}));
+      return dataFile(
+        "GeneratedTagTrace.winscope",
+        data.map((x) => x.timestamp),
+        data,
+        blobUrl,
+        FILE_TYPES.TAG_TRACE
+      );
+    },
   },
   computed: {
     files() {
       return this.$store.getters.sortedFiles.map(file => {
-        if (this.hasDataView(file)) file.show = true;
+        if (this.hasDataView(file)) {
+          file.show = true;
+        }
         return file;
       });
     },
@@ -257,6 +316,11 @@
     timelineFiles() {
       return this.$store.getters.timelineFiles;
     },
+    canGenerateTags() {
+      const fileTypes = this.dataViewFiles.map((file) => file.type);
+      return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER)
+        && fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER);
+    },
   },
   watch: {
     title() {
diff --git a/tools/winscope/src/DataAdb.vue b/tools/winscope/src/DataAdb.vue
index 0c34f26..1cffa61 100644
--- a/tools/winscope/src/DataAdb.vue
+++ b/tools/winscope/src/DataAdb.vue
@@ -31,7 +31,7 @@
         <p>Or get it from the AOSP repository.</p>
       </div>
       <div class="md-layout">
-        <md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button>
+        <md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button>
         <md-button class="md-accent" @click="restart">Retry</md-button>
       </div>
     </md-card-content>
@@ -306,8 +306,10 @@
       if (requested.length < 1) {
         this.errorText = 'No targets selected';
         this.status = STATES.ERROR;
+        this.newEventOccurred("No targets selected");
         return;
       }
+      this.newEventOccurred("Start Trace");
       this.callProxy('POST', PROXY_ENDPOINTS.CONFIG_TRACE + this.deviceId() + '/', this, null, null, requestedConfig);
       this.status = STATES.END_TRACE;
       this.callProxy('POST', PROXY_ENDPOINTS.START_TRACE + this.deviceId() + '/', this, function(request, view) {
@@ -315,10 +317,12 @@
       }, null, requested);
     },
     dumpState() {
+      this.buttonClicked("Dump State");
       const requested = this.toDump();
       if (requested.length < 1) {
         this.errorText = 'No targets selected';
         this.status = STATES.ERROR;
+        this.newEventOccurred("No targets selected");
         return;
       }
       this.status = STATES.LOAD_DATA;
@@ -331,6 +335,7 @@
       this.callProxy('POST', PROXY_ENDPOINTS.END_TRACE + this.deviceId() + '/', this, function(request, view) {
         view.loadFile(view.toTrace(), 0);
       });
+      this.newEventOccurred("Ended Trace");
     },
     loadFile(files, idx) {
       this.callProxy('GET', PROXY_ENDPOINTS.FETCH + this.deviceId() + '/' + files[idx] + '/', this, function(request, view) {
@@ -388,9 +393,11 @@
       return this.selectedDevice;
     },
     restart() {
+      this.buttonClicked("Connect / Retry");
       this.status = STATES.CONNECTING;
     },
     resetLastDevice() {
+      this.buttonClicked("Change Device");
       this.adbStore.lastDevice = '';
       this.restart();
     },
diff --git a/tools/winscope/src/DataInput.vue b/tools/winscope/src/DataInput.vue
index 6bd7e35..c55bc99 100644
--- a/tools/winscope/src/DataInput.vue
+++ b/tools/winscope/src/DataInput.vue
@@ -19,25 +19,41 @@
       <div class="md-title">Open files</div>
     </md-card-header>
     <md-card-content>
-      <md-list>
-        <md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
-          <md-icon>{{FILE_ICONS[file.type]}}</md-icon>
-          <span class="md-list-item-text">{{file.filename}} ({{file.type}})
-          </span>
-          <md-button
-            class="md-icon-button md-accent"
-            @click="onRemoveFile(file.type)"
-          >
-            <md-icon>close</md-icon>
-          </md-button>
-        </md-list-item>
-      </md-list>
-      <md-progress-spinner
-        :md-diameter="30"
-        :md-stroke="3"
-        md-mode="indeterminate"
-        v-show="loadingFiles"
-      />
+      <div class="dropbox">
+        <md-list style="background: none">
+          <md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
+            <md-icon>{{FILE_ICONS[file.type]}}</md-icon>
+            <span class="md-list-item-text">{{file.filename}} ({{file.type}})
+            </span>
+            <md-button
+              class="md-icon-button md-accent"
+              @click="onRemoveFile(file.type)"
+            >
+              <md-icon>close</md-icon>
+            </md-button>
+          </md-list-item>
+        </md-list>
+        <md-progress-spinner
+          :md-diameter="30"
+          :md-stroke="3"
+          md-mode="indeterminate"
+          v-show="loadingFiles"
+          class="progress-spinner"
+        />
+        <input
+          type="file"
+          @change="onLoadFile"
+          v-on:drop="handleFileDrop"
+          ref="fileUpload"
+          id="dropzone"
+          v-show="false"
+          multiple
+        />
+          <p v-if="!dataReady">
+            Drag your <b>.winscope</b> or <b>.zip</b> file(s) here to begin
+          </p>
+        </div>
+
       <div class="md-layout">
         <div class="md-layout-item md-small-size-100">
           <md-field>
@@ -53,13 +69,6 @@
         </div>
       </div>
       <div class="md-layout">
-        <input
-          type="file"
-          @change="onLoadFile"
-          ref="fileUpload"
-          v-show="false"
-          :multiple="fileType === 'auto'"
-        />
         <md-button
           class="md-primary md-theme-default"
           @click="$refs.fileUpload.click()"
@@ -143,6 +152,7 @@
     },
     hideSnackbarMessage() {
       this.showSnackbar = false;
+      this.buttonClicked("Hide Snackbar Message")
     },
     getFetchFilesLoadingAnimation() {
       let frame = 0;
@@ -226,20 +236,24 @@
         });
       }
     },
-    fileDragIn(e){
+    fileDragIn(e) {
       e.preventDefault();
     },
-    fileDragOut(e){
+    fileDragOut(e) {
       e.preventDefault();
     },
     handleFileDrop(e) {
       e.preventDefault();
       let droppedFiles = e.dataTransfer.files;
       if(!droppedFiles) return;
+      // Record analytics event
+      this.draggedAndDropped(droppedFiles);
+
       this.processFiles(droppedFiles);
     },
     onLoadFile(e) {
       const files = event.target.files || event.dataTransfer.files;
+      this.uploadedFileThroughFilesystem(files);
       this.processFiles(files);
     },
     async processFiles(files) {
@@ -534,4 +548,30 @@
   },
 };
 
-</script>
\ No newline at end of file
+</script>
+<style>
+  .dropbox:hover {
+      background: rgb(224, 224, 224);
+    }
+
+  .dropbox p {
+    font-size: 1.2em;
+    text-align: center;
+    padding: 50px 10px;
+  }
+
+  .dropbox {
+    outline: 2px dashed #448aff; /* the dash box */
+    outline-offset: -10px;
+    background: white;
+    color: #448aff;
+    padding: 10px 10px 10px 10px;
+    min-height: 200px; /* minimum height */
+    position: relative;
+    cursor: pointer;
+  }
+
+  .progress-spinner {
+    display: block;
+  }
+</style>
\ No newline at end of file
diff --git a/tools/winscope/src/DataView.vue b/tools/winscope/src/DataView.vue
index a2751f1..5250824 100644
--- a/tools/winscope/src/DataView.vue
+++ b/tools/winscope/src/DataView.vue
@@ -16,13 +16,13 @@
   <div @click="onClick($event)">
     <flat-card v-if="hasDataView(file)">
       <md-card-header>
-        <button class="toggle-view-button" @click="toggleView">
-          <i aria-hidden="true" class="md-icon md-theme-default material-icons">
-            {{ isShowFileType(file.type) ? "expand_more" : "chevron_right" }}
-          </i>
-        </button>
         <md-card-header-text>
           <div class="md-title">
+            <button class="toggle-view-button" @click="toggleView">
+              <i aria-hidden="true" class="md-icon md-theme-default material-icons">
+                {{ isShowFileType(file.type) ? "expand_more" : "chevron_right" }}
+              </i>
+            </button>
             <md-icon>{{ TRACE_ICONS[file.type] }}</md-icon>
             {{ file.type }}
           </div>
@@ -160,6 +160,7 @@
       // Pass click event to parent, so that click event handler can be attached
       // to component.
       this.$emit('click', e);
+      this.newEventOccurred(e.toString());
     },
     /** Filter data view files by current show settings */
     updateShowFileTypes() {
diff --git a/tools/winscope/src/DefaultTreeElement.vue b/tools/winscope/src/DefaultTreeElement.vue
index 96c7dcf..ac03f9b 100644
--- a/tools/winscope/src/DefaultTreeElement.vue
+++ b/tools/winscope/src/DefaultTreeElement.vue
@@ -52,7 +52,7 @@
     </div>
     <div class="flicker-tags" v-for="error in errors" :key="error.message">
       <Arrow class="error-arrow"/>
-      <md-tooltip md-direction="right"> Error: {{error.message}} </md-tooltip>
+      <md-tooltip md-direction="right"> {{errorTooltip(error.message)}} </md-tooltip>
     </div>
   </span>
 </template>
@@ -80,6 +80,12 @@
     transitionTooltip(transition) {
       return transitionMap.get(transition).desc;
     },
+    errorTooltip(errorMessage) {
+      if (errorMessage.length>100) {
+        return `Error: ${errorMessage.substring(0,100)}...`;
+      }
+      return `Error: ${errorMessage}`;
+    },
   },
   components: {
     Arrow,
diff --git a/tools/winscope/src/Overlay.vue b/tools/winscope/src/Overlay.vue
index d098dd4..a75f4ec 100644
--- a/tools/winscope/src/Overlay.vue
+++ b/tools/winscope/src/Overlay.vue
@@ -65,6 +65,13 @@
             md-elevation="0"
             class="md-transparent">
 
+            <md-button
+              @click="toggleSearch()"
+              class="drop-search"
+            >
+              Toggle search bar
+            </md-button>
+
             <div class="toolbar" :class="{ expanded: expanded }">
               <div class="resize-bar" v-show="expanded">
                 <div v-if="video" @mousedown="resizeBottomNav">
@@ -75,11 +82,6 @@
                 </div>
               </div>
 
-              <md-button
-                @click="toggleSearch()"
-                class="drop-search"
-              >Show/hide search bar</md-button>
-
               <div class="active-timeline" v-show="minimized">
                 <div
                   class="active-timeline-icon"
@@ -149,14 +151,20 @@
                 v-show="minimized"
                 v-if="hasTimeline"
               >
-                <label>
-                  {{ seekTime }}
-                </label>
+                <input
+                  class="timestamp-search-input"
+                  v-model="searchInput"
+                  spellcheck="false"
+                  :placeholder="seekTime"
+                  @focus="updateInputMode(true)"
+                  @blur="updateInputMode(false)"
+                  @keyup.enter="updateSearchForTimestamp"
+                />
                 <timeline
                   :store="store"
                   :flickerMode="flickerMode"
-                  :tags="Object.freeze(tags)"
-                  :errors="Object.freeze(errors)"
+                  :tags="Object.freeze(presentTags)"
+                  :errors="Object.freeze(presentErrors)"
                   :timeline="Object.freeze(minimizedTimeline.timeline)"
                   :selected-index="minimizedTimeline.selectedIndex"
                   :scale="scale"
@@ -188,11 +196,11 @@
               >
                 <md-icon v-if="minimized">
                   expand_less
-                  <md-tooltip md-direction="top">Expand timeline</md-tooltip>
+                  <md-tooltip md-direction="top" @click="buttonClicked(`Expand Timeline`)">Expand timeline</md-tooltip>
                 </md-icon>
                 <md-icon v-else>
                   expand_more
-                  <md-tooltip md-direction="top">Collapse timeline</md-tooltip>
+                  <md-tooltip md-direction="top" @click="buttonClicked(`Collapse Timeline`)">Collapse timeline</md-tooltip>
                 </md-icon>
               </md-button>
             </div>
@@ -213,7 +221,17 @@
                 :style="`padding-top: ${resizeOffset}px;`"
               >
                 <div class="seek-time" v-if="seekTime">
-                  <b>Seek time</b>: {{ seekTime }}
+                  <b>Seek time: </b>
+                  <input
+                    class="timestamp-search-input"
+                    :class="{ expanded: expanded }"
+                    v-model="searchInput"
+                    spellcheck="false"
+                    :placeholder="seekTime"
+                    @focus="updateInputMode(true)"
+                    @blur="updateInputMode(false)"
+                    @keyup.enter="updateSearchForTimestamp"
+                  />
                 </div>
 
                 <timelines
@@ -283,14 +301,14 @@
 import Searchbar from './Searchbar.vue';
 import FileType from './mixins/FileType.js';
 import {NAVIGATION_STYLE} from './utils/consts';
-import {TRACE_ICONS, FILE_TYPES} from '@/decode.js';
+import {TRACE_ICONS} from '@/decode.js';
 
 // eslint-disable-next-line camelcase
-import {nanos_to_string} from './transform.js';
+import {nanos_to_string, getClosestTimestamp} from './transform.js';
 
 export default {
   name: 'overlay',
-  props: ['store', 'presentTags', 'presentErrors', 'tagAndErrorTraces', 'searchTypes'],
+  props: ['store', 'presentTags', 'presentErrors', 'searchTypes'],
   mixins: [FileType],
   data() {
     return {
@@ -312,8 +330,8 @@
       cropIntent: null,
       TRACE_ICONS,
       search: false,
-      tags: [],
-      errors: [],
+      searchInput: "",
+      isSeekTimeInputMode: false,
     };
   },
   created() {
@@ -326,6 +344,7 @@
   },
   destroyed() {
     this.$store.commit('removeMergedTimeline', this.mergedTimeline);
+    this.updateInputMode(false);
   },
   watch: {
     navigationStyle(style) {
@@ -433,8 +452,6 @@
       }
     },
     minimizedTimeline() {
-      this.updateFlickerMode(this.navigationStyle);
-
       if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
         return this.mergedTimeline;
       }
@@ -471,7 +488,7 @@
       return this.timelineFiles.length > 1;
     },
     flickerMode() {
-      return this.tags.length>0 || this.errors.length>0;
+      return this.presentTags.length>0 || this.presentErrors.length>0;
     },
   },
   updated() {
@@ -486,7 +503,29 @@
   methods: {
     toggleSearch() {
       this.search = !(this.search);
+      this.buttonClicked("Toggle Search Bar");
     },
+    /**
+     * determines whether left/right arrow keys should move cursor in input field
+     * and upon click of input field, fills with current timestamp
+     */
+    updateInputMode(isInputMode) {
+      this.isSeekTimeInputMode = isInputMode;
+      this.store.isInputMode = isInputMode;
+      if (!isInputMode) {
+        this.searchInput = "";
+      } else {
+        this.searchInput = this.seekTime;
+      }
+    },
+    /** Navigates to closest timestamp in timeline to search input*/
+    updateSearchForTimestamp() {
+      const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
+      this.$store.dispatch("updateTimelineTime", closestTimestamp);
+      this.updateInputMode(false);
+      this.newEventOccurred("Searching for timestamp")
+    },
+
     emitBottomHeightUpdate() {
       if (this.$refs.bottomNav) {
         const newHeight = this.$refs.bottomNav.$el.clientHeight;
@@ -599,12 +638,15 @@
     },
     closeVideoOverlay() {
       this.showVideoOverlay = false;
+      this.buttonClicked("Close Video Overlay")
     },
     openVideoOverlay() {
       this.showVideoOverlay = true;
+      this.buttonClicked("Open Video Overlay")
     },
     toggleVideoOverlay() {
       this.showVideoOverlay = !this.showVideoOverlay;
+      this.buttonClicked("Toggle Video Overlay")
     },
     videoLoaded() {
       this.$refs.videoOverlay.contentLoaded();
@@ -647,43 +689,6 @@
 
       this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
     },
-    updateFlickerMode(style) {
-      if (style === NAVIGATION_STYLE.GLOBAL ||
-        style === NAVIGATION_STYLE.CUSTOM) {
-        this.tags = this.presentTags;
-        this.errors = this.presentErrors;
-
-      } else if (style === NAVIGATION_STYLE.FOCUSED) {
-        if (this.focusedFile.timeline) {
-          this.tags = this.getTagTimelineComponents(this.presentTags, this.focusedFile);
-          this.errors = this.getTagTimelineComponents(this.presentErrors, this.focusedFile);
-        }
-      } else if (
-        style.split('-').length >= 2 &&
-        style.split('-')[0] === NAVIGATION_STYLE.TARGETED
-      ) {
-        const file = this.$store.state.traces[style.split('-')[1]];
-        if (file.timeline) {
-          this.tags = this.getTagTimelineComponents(this.presentTags, file);
-          this.errors = this.getTagTimelineComponents(this.presentErrors, file);
-        }
-      //Unexpected navigation type or no timeline present in file
-      } else {
-        console.warn('Unexpected timeline or navigation type; no flicker mode available');
-        this.tags = [];
-        this.errors = [];
-      }
-    },
-    getTagTimelineComponents(items, file) {
-      if (file.type===FILE_TYPES.SURFACE_FLINGER_TRACE) {
-        return items.filter(item => item.layerId !== -1);
-      }
-      if (file.type===FILE_TYPES.WINDOW_MANAGER_TRACE) {
-        return items.filter(item => item.taskId !== -1);
-      }
-      // if focused file is not one supported by tags/errors
-      return [];
-    },
     updateVideoOverlayWidth(width) {
       this.videoOverlayExtraWidth = width;
     },
@@ -896,6 +901,7 @@
   color: rgba(0,0,0,0.54);
   font-size: 12px;
   font-family: inherit;
+  cursor: text;
 }
 
 .minimized-timeline-content .minimized-timeline {
@@ -921,6 +927,27 @@
   cursor: help;
 }
 
+.timestamp-search-input {
+  outline: none;
+  border-width: 0 0 1px;
+  border-color: gray;
+  font-family: inherit;
+  color: #448aff;
+  font-size: 12px;
+  padding: 0;
+  letter-spacing: inherit;
+  width: 125px;
+}
+
+.timestamp-search-input:focus {
+  border-color: #448aff;
+}
+
+.timestamp-search-input.expanded {
+  font-size: 14px;
+  width: 150px;
+}
+
 .drop-search:hover {
   background-color: #9af39f;
 }
diff --git a/tools/winscope/src/Searchbar.vue b/tools/winscope/src/Searchbar.vue
index 50aca7b..bbfaa0a 100644
--- a/tools/winscope/src/Searchbar.vue
+++ b/tools/winscope/src/Searchbar.vue
@@ -14,82 +14,114 @@
 -->
 <template>
   <md-content class="searchbar">
-    <div class="search-timestamp" v-if="isTimestampSearch()">
-      <md-button
-        class="search-timestamp-button"
-        @click="updateSearchForTimestamp"
-      >
-        Navigate to timestamp
-      </md-button>
-      <md-field class="search-input">
-        <label>Enter timestamp</label>
-        <md-input v-model="searchInput" @keyup.enter.native="updateSearchForTimestamp" />
-      </md-field>
-    </div>
 
-    <div class="dropdown-content" v-if="isTagSearch()">
-      <table>
-        <tr class="header">
-          <th style="width: 10%">Global Start</th>
-          <th style="width: 10%">Global End</th>
-          <th style="width: 80%">Description</th>
-        </tr>
-
-        <tr v-for="item in filteredTransitionsAndErrors" :key="item">
-          <td
-            v-if="isTransition(item)"
-            class="inline-time"
-            @click="
-              setCurrentTimestamp(transitionStart(transitionTags(item.id)))
-            "
-          >
-            <span>{{ transitionTags(item.id)[0].desc }}</span>
-          </td>
-          <td
-            v-if="isTransition(item)"
-            class="inline-time"
-            @click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
-          >
-            <span>{{ transitionTags(item.id)[1].desc }}</span>
-          </td>
-          <td
-            v-if="isTransition(item)"
-            class="inline-transition"
-            :style="{color: transitionTextColor(item.transition)}"
-            @click="
-              setCurrentTimestamp(transitionStart(transitionTags(item.id)))
-            "
-          >
-            {{ transitionDesc(item.transition) }}
-          </td>
-
-          <td
-            v-if="!isTransition(item)"
-            class="inline-time"
-            @click="setCurrentTimestamp(item.timestamp)"
-          >
-            {{ errorDesc(item.timestamp) }}
-          </td>
-          <td v-if="!isTransition(item)">-</td>
-          <td
-            v-if="!isTransition(item)"
-            class="inline-error"
-            @click="setCurrentTimestamp(item.timestamp)"
-          >
-            Error: {{item.message}}
-          </td>
-        </tr>
-      </table>
-      <md-field class="search-input">
-        <label
-          >Filter by transition or error message. Click to navigate to closest
-          timestamp in active timeline.</label
+    <div class="tabs">
+      <div class="search-timestamp" v-if="isTimestampSearch()">
+        <md-field md-inline class="search-input">
+          <label>Enter timestamp</label>
+          <md-input
+            v-model="searchInput"
+            v-on:focus="updateInputMode(true)"
+            v-on:blur="updateInputMode(false)"
+            @keyup.enter.native="updateSearchForTimestamp"
+          />
+        </md-field>
+        <md-button
+          class="md-dense md-primary search-timestamp-button"
+          @click="updateSearchForTimestamp"
         >
-        <md-input v-model="searchInput"></md-input>
-      </md-field>
+          Go to timestamp
+        </md-button>
+      </div>
+
+      <div class="dropdown-content" v-if="isTransitionSearch()">
+        <table>
+          <tr class="header">
+            <th style="width: 10%">Global Start</th>
+            <th style="width: 10%">Global End</th>
+            <th style="width: 80%">Transition</th>
+          </tr>
+
+          <tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
+            <td
+              v-if="isTransition(item)"
+              class="inline-time"
+              @click="
+                setCurrentTimestamp(transitionStart(transitionTags(item.id)))
+              "
+            >
+              <span>{{ transitionTags(item.id)[0].desc }}</span>
+            </td>
+            <td
+              v-if="isTransition(item)"
+              class="inline-time"
+              @click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
+            >
+              <span>{{ transitionTags(item.id)[1].desc }}</span>
+            </td>
+            <td
+              v-if="isTransition(item)"
+              class="inline-transition"
+              :style="{color: transitionTextColor(item.transition)}"
+              @click="setCurrentTimestamp(transitionStart(transitionTags(item.id)))"
+            >
+              {{ transitionDesc(item.transition) }}
+            </td>
+          </tr>
+        </table>
+        <md-field md-inline class="search-input">
+          <label>
+            Filter by transition name. Click to navigate to closest
+            timestamp in active timeline.
+          </label>
+          <md-input
+            v-model="searchInput"
+            v-on:focus="updateInputMode(true)"
+            v-on:blur="updateInputMode(false)"
+          />
+        </md-field>
+      </div>
+
+      <div class="dropdown-content" v-if="isErrorSearch()">
+        <table>
+          <tr class="header">
+            <th style="width: 10%">Timestamp</th>
+            <th style="width: 90%">Error Message</th>
+          </tr>
+
+          <tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
+            <td
+              v-if="!isTransition(item)"
+              class="inline-time"
+              @click="setCurrentTimestamp(item.timestamp)"
+            >
+              {{ errorDesc(item.timestamp) }}
+            </td>
+            <td
+              v-if="!isTransition(item)"
+              class="inline-error"
+              @click="setCurrentTimestamp(item.timestamp)"
+            >
+              {{item.message}}
+            </td>
+          </tr>
+        </table>
+        <md-field md-inline class="search-input">
+          <label>
+            Filter by error message. Click to navigate to closest
+            timestamp in active timeline.
+          </label>
+          <md-input
+            v-model="searchInput"
+            v-on:focus="updateInputMode(true)"
+            v-on:blur="updateInputMode(false)"
+          />
+        </md-field>
+      </div>
     </div>
 
-    <div class="tab-container">
+    <div class="tab-container" v-if="searchTypes.length > 0">
+      Search mode:
       <md-button
         v-for="searchType in searchTypes"
         :key="searchType"
@@ -103,9 +135,7 @@
 </template>
 <script>
 import { transitionMap, SEARCH_TYPE } from "./utils/consts";
-import { nanos_to_string, string_to_nanos } from "./transform";
-
-const regExpTimestampSearch = new RegExp(/^\d+$/);
+import { nanos_to_string, getClosestTimestamp } from "./transform";
 
 export default {
   name: "searchbar",
@@ -132,7 +162,8 @@
       var tags = [];
       var filter = this.searchInput.toUpperCase();
       this.presentTags.forEach((tag) => {
-        if (tag.transition.includes(filter)) tags.push(tag);
+        const tagTransition = tag.transition.toUpperCase();
+        if (tagTransition.includes(filter)) tags.push(tag);
       });
       return tags;
     },
@@ -141,7 +172,8 @@
       var tagsAndErrors = [...this.filteredTags()];
       var filter = this.searchInput.toUpperCase();
       this.presentErrors.forEach((error) => {
-        if (error.message.includes(filter)) tagsAndErrors.push(error);
+        const errorMessage = error.message.toUpperCase();
+        if (errorMessage.includes(filter)) tagsAndErrors.push(error);
       });
       // sort into chronological order
       tagsAndErrors.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1));
@@ -170,8 +202,10 @@
       var times = tags.map((tag) => tag.timestamp);
       return times[times.length - 1];
     },
-    /** Upon selecting a start/end tag in the dropdown;
-     * navigates to that timestamp in the timeline */
+    /**
+     * Upon selecting a start/end tag in the dropdown;
+     * navigates to that timestamp in the timeline
+     */
     setCurrentTimestamp(timestamp) {
       this.$store.dispatch("updateTimelineTime", timestamp);
     },
@@ -191,22 +225,15 @@
 
     /** Navigates to closest timestamp in timeline to search input*/
     updateSearchForTimestamp() {
-      if (regExpTimestampSearch.test(this.searchInput)) {
-        var roundedTimestamp = parseInt(this.searchInput);
-      } else {
-        var roundedTimestamp = string_to_nanos(this.searchInput);
-      }
-      var closestTimestamp = this.timeline.reduce(function (prev, curr) {
-        return Math.abs(curr - roundedTimestamp) <
-          Math.abs(prev - roundedTimestamp)
-          ? curr
-          : prev;
-      });
+      const closestTimestamp = getClosestTimestamp(this.searchInput, this.timeline);
       this.setCurrentTimestamp(closestTimestamp);
     },
 
-    isTagSearch() {
-      return this.searchType === SEARCH_TYPE.TAG;
+    isTransitionSearch() {
+      return this.searchType === SEARCH_TYPE.TRANSITIONS;
+    },
+    isErrorSearch() {
+      return this.searchType === SEARCH_TYPE.ERRORS;
     },
     isTimestampSearch() {
       return this.searchType === SEARCH_TYPE.TIMESTAMP;
@@ -214,6 +241,11 @@
     isTransition(item) {
       return item.stacktrace === undefined;
     },
+
+    /** determines whether left/right arrow keys should move cursor in input field */
+    updateInputMode(isInputMode) {
+      this.store.isInputMode = isInputMode;
+    },
   },
   computed: {
     filteredTransitionsAndErrors() {
@@ -227,9 +259,12 @@
       });
     },
   },
+  destroyed() {
+    this.updateInputMode(false);
+  },
 };
 </script>
-<style>
+<style scoped>
 .searchbar {
   background-color: rgb(250, 243, 233) !important;
   top: 0;
@@ -241,8 +276,14 @@
   bottom: 1px;
 }
 
+.tabs {
+  padding-top: 1rem;
+}
+
 .tab-container {
-  padding: 0px 20px 0px 20px;
+  padding-left: 20px;
+  display: flex;
+  align-items: center;
 }
 
 .tab.active {
@@ -255,11 +296,18 @@
 
 .search-timestamp {
   padding: 5px 20px 0px 20px;
-  display: block;
+  display: inline-flex;
+  width: 100%;
+}
+
+.search-timestamp > .search-input {
+  margin-top: -5px;
+  max-width: 200px;
 }
 
 .search-timestamp-button {
   left: 0;
+  padding: 0 15px;
 }
 
 .dropdown-content {
@@ -269,7 +317,7 @@
 
 .dropdown-content table {
   overflow-y: scroll;
-  height: 150px;
+  max-height: 150px;
   display: block;
 }
 
@@ -283,7 +331,7 @@
 }
 
 .inline-time:hover {
-  background: #9af39f;
+  background: rgb(216, 250, 218);
   cursor: pointer;
 }
 
@@ -292,7 +340,7 @@
 }
 
 .inline-transition:hover {
-  background: #9af39f;
+  background: rgb(216, 250, 218);
   cursor: pointer;
 }
 
@@ -302,7 +350,7 @@
 }
 
 .inline-error:hover {
-  background: #9af39f;
+  background: rgb(216, 250, 218);
   cursor: pointer;
 }
 </style>
diff --git a/tools/winscope/src/Timeline.vue b/tools/winscope/src/Timeline.vue
index e5a3ce8..5b373d2 100644
--- a/tools/winscope/src/Timeline.vue
+++ b/tools/winscope/src/Timeline.vue
@@ -57,12 +57,13 @@
       />
       <line
         v-for="error in errorPositions"
-        :key="error"
-        :x1="`${error}%`"
-        :x2="`${error}%`"
+        :key="error.pos"
+        :x1="`${error.pos}%`"
+        :x2="`${error.pos}%`"
         y1="0"
         y2="18px"
         class="error"
+        @click="onErrorClick(error.ts)"
       />
     </svg>
   </div>
@@ -139,6 +140,7 @@
 }
 .error {
   stroke: rgb(255, 0, 0);
-  stroke-width: 2px;
+  stroke-width: 8px;
+  cursor: pointer;
 }
 </style>
\ No newline at end of file
diff --git a/tools/winscope/src/TraceView.vue b/tools/winscope/src/TraceView.vue
index ab77d35..ef42d65 100644
--- a/tools/winscope/src/TraceView.vue
+++ b/tools/winscope/src/TraceView.vue
@@ -45,7 +45,11 @@
           <md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">Flicker</md-checkbox>
           <md-field md-inline class="filter">
             <label>Filter...</label>
-            <md-input v-model="hierarchyPropertyFilterString"></md-input>
+            <md-input
+              v-model="hierarchyPropertyFilterString"
+              v-on:focus="updateInputMode(true)"
+              v-on:blur="updateInputMode(false)"
+            />
           </md-field>
         </md-content>
         <div class="tree-view-wrapper">
@@ -98,7 +102,11 @@
           </md-checkbox>
           <md-field md-inline class="filter">
             <label>Filter...</label>
-            <md-input v-model="propertyFilterString"></md-input>
+            <md-input
+              v-model="propertyFilterString"
+              v-on:focus="updateInputMode(true)"
+              v-on:blur="updateInputMode(false)"
+            />
           </md-field>
         </md-content>
         <div class="properties-content">
@@ -138,7 +146,7 @@
 import {ObjectTransformer} from './transform.js';
 import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js';
 import {TRACE_TYPES, DUMP_TYPES} from './decode.js';
-import {stableIdCompatibilityFixup} from './utils/utils.js';
+import {isPropertyMatch, stableIdCompatibilityFixup} from './utils/utils.js';
 import {CompatibleFeatures} from './utils/compatibility.js';
 import {getPropertiesForDisplay} from './flickerlib/mixin';
 import ObjectFormatter from './flickerlib/ObjectFormatter';
@@ -318,9 +326,7 @@
     matchItems(flickerItems, entryItem) {
       var match = false;
       flickerItems.forEach(flickerItem => {
-        if (flickerItem.taskId===entryItem.taskId || flickerItem.layerId===entryItem.id) {
-          match = true;
-        }
+        if (isPropertyMatch(flickerItem, entryItem)) match = true;
       });
       return match;
     },
@@ -328,6 +334,11 @@
     isEntryTagMatch(entryItem) {
       return this.matchItems(this.presentTags, entryItem) || this.matchItems(this.presentErrors, entryItem);
     },
+
+    /** determines whether left/right arrow keys should move cursor in input field */
+    updateInputMode(isInputMode) {
+      this.store.isInputMode = isInputMode;
+    },
   },
   created() {
     this.setData(this.file.data[this.file.selectedIndex ?? 0]);
diff --git a/tools/winscope/src/TreeView.vue b/tools/winscope/src/TreeView.vue
index 846924e..e19643c 100644
--- a/tools/winscope/src/TreeView.vue
+++ b/tools/winscope/src/TreeView.vue
@@ -122,6 +122,7 @@
 import DefaultTreeElement from './DefaultTreeElement.vue';
 import NodeContextMenu from './NodeContextMenu.vue';
 import {DiffType} from './utils/diff.js';
+import {isPropertyMatch} from './utils/utils.js';
 
 /* in px, must be kept in sync with css, maybe find a better solution... */
 const levelOffset = 24;
@@ -220,6 +221,9 @@
     },
     toggleTree() {
       this.setCollapseValue(!this.isCollapsed);
+      if (!this.isCollapsed) {
+        this.openedToSeeAttributeField(this.item.name)
+      }
     },
     expandTree() {
       this.setCollapseValue(false);
@@ -446,17 +450,13 @@
       }
     },
 
-    /** Check if tag/error id matches entry id */
-    isIdMatch(a, b) {
-      return a.taskId===b.taskId || a.layerId===b.id;
-    },
     /** Performs check for id match between entry and present tags/errors
      * exits once match has been found
      */
     matchItems(flickerItems) {
       var match = false;
       flickerItems.every(flickerItem => {
-        if (this.isIdMatch(flickerItem, this.item)) {
+        if (isPropertyMatch(flickerItem, this.item)) {
           match = true;
           return false;
         }
@@ -476,7 +476,7 @@
       var transitions = [];
       var ids = [];
       this.currentTags.forEach(tag => {
-        if (!ids.includes(tag.id) && this.isIdMatch(tag, this.item)) {
+        if (!ids.includes(tag.id) && isPropertyMatch(tag, this.item)) {
           transitions.push(tag.transition);
           ids.push(tag.id);
         }
@@ -484,7 +484,7 @@
       return transitions;
     },
     getCurrentErrorTags() {
-      return this.currentErrors.filter(error => this.isIdMatch(error, this.item));
+      return this.currentErrors.filter(error => isPropertyMatch(error, this.item));
     },
   },
   computed: {
diff --git a/tools/winscope/src/decode.js b/tools/winscope/src/decode.js
index 577993f..dde1041 100644
--- a/tools/winscope/src/decode.js
+++ b/tools/winscope/src/decode.js
@@ -554,6 +554,14 @@
 
 function protoDecoder(buffer, params, fileName, store) {
   const transformed = decodeAndTransformProto(buffer, params, store.displayDefaults);
+
+  // add tagGenerationTrace to dataFile for WM/SF traces so tags can be generated
+  var tagGenerationTrace = null;
+  if (params.type === FILE_TYPES.WINDOW_MANAGER_TRACE ||
+    params.type === FILE_TYPES.SURFACE_FLINGER_TRACE) {
+    tagGenerationTrace = transformed;
+  }
+
   let data;
   if (params.timeline) {
     data = transformed.entries ?? transformed.children;
@@ -561,7 +569,15 @@
     data = [transformed];
   }
   const blobUrl = URL.createObjectURL(new Blob([buffer], {type: params.mime}));
-  return dataFile(fileName, data.map((x) => x.timestamp), data, blobUrl, params.type);
+
+  return dataFile(
+    fileName,
+    data.map((x) => x.timestamp),
+    data,
+    blobUrl,
+    params.type,
+    tagGenerationTrace
+  );
 }
 
 function videoDecoder(buffer, params, fileName, store) {
@@ -570,7 +586,7 @@
   return dataFile(fileName, timeline, blobUrl, blobUrl, params.type);
 }
 
-function dataFile(filename, timeline, data, blobUrl, type) {
+function dataFile(filename, timeline, data, blobUrl, type, tagGenerationTrace = null) {
   return {
     filename: filename,
     // Object is frozen for performance reasons
@@ -578,6 +594,7 @@
     timeline: Object.freeze(timeline),
     data: data,
     blobUrl: blobUrl,
+    tagGenerationTrace: tagGenerationTrace,
     type: type,
     selectedIndex: 0,
     destroy() {
@@ -678,4 +695,17 @@
  */
 class UndetectableFileType extends Error { }
 
-export {detectAndDecode, decodeAndTransformProto, FILE_TYPES, TRACE_INFO, TRACE_TYPES, DUMP_TYPES, DUMP_INFO, FILE_DECODERS, FILE_ICONS, UndetectableFileType};
+export {
+  dataFile,
+  detectAndDecode,
+  decodeAndTransformProto,
+  TagTraceMessage,
+  FILE_TYPES,
+  TRACE_INFO,
+  TRACE_TYPES,
+  DUMP_TYPES,
+  DUMP_INFO,
+  FILE_DECODERS,
+  FILE_ICONS,
+  UndetectableFileType
+};
diff --git a/tools/winscope/src/flickerlib/WindowManagerState.ts b/tools/winscope/src/flickerlib/WindowManagerState.ts
index 56b3181..0f13254 100644
--- a/tools/winscope/src/flickerlib/WindowManagerState.ts
+++ b/tools/winscope/src/flickerlib/WindowManagerState.ts
@@ -46,7 +46,7 @@
         proto.rootWindowContainer.pendingActivities.map(it => it.title),
         rootWindowContainer,
         keyguardControllerState,
-        timestamp = timestamp
+        /*timestamp */ `${timestamp}`
     );
 
     addAttributes(entry, proto);
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
index d753a70..fefb27f 100644
--- a/tools/winscope/src/flickerlib/common.js
+++ b/tools/winscope/src/flickerlib/common.js
@@ -88,6 +88,9 @@
 const ErrorState = require('flicker').com.android.server.wm.traces.common.errors.ErrorState;
 const ErrorTrace = require('flicker').com.android.server.wm.traces.common.errors.ErrorTrace;
 
+// Service
+const TaggingEngine = require('flicker').com.android.server.wm.traces.common.service.TaggingEngine;
+
 const EMPTY_BUFFER = new Buffer(0, 0, 0, 0);
 const EMPTY_COLOR = new Color(-1, -1, -1, 0);
 const EMPTY_RECT = new Rect(0, 0, 0, 0);
@@ -255,6 +258,8 @@
   Rect,
   RectF,
   Region,
+  // Service
+  TaggingEngine,
   toSize,
   toBuffer,
   toColor,
diff --git a/tools/winscope/src/flickerlib/errors/ErrorState.ts b/tools/winscope/src/flickerlib/errors/ErrorState.ts
index 78364c6..533af23 100644
--- a/tools/winscope/src/flickerlib/errors/ErrorState.ts
+++ b/tools/winscope/src/flickerlib/errors/ErrorState.ts
@@ -19,7 +19,7 @@
 
 ErrorState.fromProto = function (protos: any[], timestamp: number): ErrorState {
     const errors = protos.map(it => Error.fromProto(it));
-    const state = new ErrorState(errors, timestamp);
+    const state = new ErrorState(errors, `${timestamp}`);
     return state;
 }
 
diff --git a/tools/winscope/src/flickerlib/tags/Tag.ts b/tools/winscope/src/flickerlib/tags/Tag.ts
index 8ecebc6..fdaa373 100644
--- a/tools/winscope/src/flickerlib/tags/Tag.ts
+++ b/tools/winscope/src/flickerlib/tags/Tag.ts
@@ -22,11 +22,14 @@
   ['ROTATION', TransitionType.ROTATION],
   ['PIP_ENTER', TransitionType.PIP_ENTER],
   ['PIP_RESIZE', TransitionType.PIP_RESIZE],
+  ['PIP_CLOSE', TransitionType.PIP_CLOSE],
   ['PIP_EXIT', TransitionType.PIP_EXIT],
   ['APP_LAUNCH', TransitionType.APP_LAUNCH],
   ['APP_CLOSE', TransitionType.APP_CLOSE],
   ['IME_APPEAR', TransitionType.IME_APPEAR],
   ['IME_DISAPPEAR', TransitionType.IME_DISAPPEAR],
+  ['APP_PAIRS_ENTER', TransitionType.APP_PAIRS_ENTER],
+  ['APP_PAIRS_EXIT', TransitionType.APP_PAIRS_EXIT],
 ]);
 
 Tag.fromProto = function (proto: any): Tag {
diff --git a/tools/winscope/src/flickerlib/tags/TagState.ts b/tools/winscope/src/flickerlib/tags/TagState.ts
index d127a2d..7a3de7b 100644
--- a/tools/winscope/src/flickerlib/tags/TagState.ts
+++ b/tools/winscope/src/flickerlib/tags/TagState.ts
@@ -19,7 +19,7 @@
 
 TagState.fromProto = function (timestamp: number, protos: any[]): TagState {
     const tags = protos.map(it => Tag.fromProto(it));
-    const state = new TagState(timestamp, tags);
+    const state = new TagState(`${timestamp}`, tags);
     return state;
 }
 
diff --git a/tools/winscope/src/flickerlib/tags/TransitionType.ts b/tools/winscope/src/flickerlib/tags/TransitionType.ts
index aebc72f..b4e376a 100644
--- a/tools/winscope/src/flickerlib/tags/TransitionType.ts
+++ b/tools/winscope/src/flickerlib/tags/TransitionType.ts
@@ -18,11 +18,14 @@
   ROTATION = 'ROTATION',
   PIP_ENTER = 'PIP_ENTER',
   PIP_RESIZE ='PIP_RESIZE',
+  PIP_CLOSE = 'PIP_CLOSE',
   PIP_EXIT = 'PIP_EXIT',
   APP_LAUNCH = 'APP_LAUNCH',
   APP_CLOSE = 'APP_CLOSE',
   IME_APPEAR = 'IME_APPEAR',
   IME_DISAPPEAR = 'IME_DISAPPEAR',
+  APP_PAIRS_ENTER = 'APP_PAIRS_ENTER',
+  APP_PAIRS_EXIT = 'APP_PAIRS_EXIT',
 };
 
 export default TransitionType;
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/Activity.ts b/tools/winscope/src/flickerlib/windows/Activity.ts
index 3d6149d..60e28cd 100644
--- a/tools/winscope/src/flickerlib/windows/Activity.ts
+++ b/tools/winscope/src/flickerlib/windows/Activity.ts
@@ -16,6 +16,7 @@
 
 import { shortenName } from '../mixin'
 import { Activity } from "../common"
+import { VISIBLE_CHIP } from '../treeview/Chips'
 import WindowContainer from "./WindowContainer"
 
 Activity.fromProto = function (proto: any): Activity {
@@ -50,6 +51,7 @@
     entry.proto = proto;
     entry.kind = entry.constructor.name;
     entry.shortName = shortenName(entry.name);
+    entry.chips = entry.isVisible ? [VISIBLE_CHIP] : [];
 }
 
 export default Activity;
diff --git a/tools/winscope/src/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
index 5fe3a57..9a99226 100644
--- a/tools/winscope/src/flickerlib/windows/WindowContainer.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
@@ -58,6 +58,7 @@
         name,
         token,
         proto.orientation,
+        proto.surfaceControl?.layerId ?? 0,
         proto.visible,
         config,
         children
diff --git a/tools/winscope/src/main.js b/tools/winscope/src/main.js
index 26a0f8e..703cced 100644
--- a/tools/winscope/src/main.js
+++ b/tools/winscope/src/main.js
@@ -17,6 +17,7 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 import VueMaterial from 'vue-material'
+import VueGtag from "vue-gtag";
 
 import App from './App.vue'
 import { TRACE_TYPES, DUMP_TYPES, TRACE_INFO, DUMP_INFO } from './decode.js'
@@ -109,6 +110,12 @@
     video(state) {
       return state.traces[TRACE_TYPES.SCREEN_RECORDING];
     },
+    tagGenerationWmTrace(state, getters) {
+      return state.traces[TRACE_TYPES.WINDOW_MANAGER].tagGenerationTrace;
+    },
+    tagGenerationSfTrace(state, getters) {
+      return state.traces[TRACE_TYPES.SURFACE_FLINGER].tagGenerationTrace;
+    }
   },
   mutations: {
     setCurrentTimestamp(state, timestamp) {
@@ -352,6 +359,61 @@
   }
 })
 
+/**
+ * Make Google analytics functionalities available for recording events.
+ */
+Vue.use(VueGtag, {
+  config: { id: 'G-RRV0M08Y76'}
+})
+
+Vue.mixin({
+  methods: {
+    buttonClicked(button) {
+      const string = "Clicked " + button + " Button";
+      this.$gtag.event(string, {
+        'event_category': 'Button Clicked',
+        'event_label': "Winscope Interactions",
+        'value': button,
+      });
+    },
+    draggedAndDropped(val) {
+      this.$gtag.event("Dragged And DroppedFile", {
+        'event_category': 'Uploaded file',
+        'event_label': "Winscope Interactions",
+        'value': val,
+      });
+    },
+    uploadedFileThroughFilesystem(val) {
+      this.$gtag.event("Uploaded File From Filesystem", {
+        'event_category': 'Uploaded file',
+        'event_label': "Winscope Interactions",
+        'value': val,
+      });
+    },
+    newEventOccurred(event) {
+      this.$gtag.event(event, {
+        'event_category': event,
+        'event_label': "Winscope Interactions",
+        'value': 1,
+      });
+    },
+    seeingNewScreen(screenname) {
+      this.$gtag.screenview({
+        app_name: "Winscope",
+        screen_name: screenname,
+      })
+    },
+    openedToSeeAttributeField(field) {
+      const string = "Opened field " + field;
+      this.$gtag.event(string, {
+        'event_category': "Opened attribute field",
+        'event_label': "Winscope Interactions",
+        'value': field,
+      });
+    },
+  }
+});
+
 new Vue({
   el: '#app',
   store, // inject the Vuex store into all components
diff --git a/tools/winscope/src/mixins/SaveAsZip.js b/tools/winscope/src/mixins/SaveAsZip.js
index dc57539..cf20fa4 100644
--- a/tools/winscope/src/mixins/SaveAsZip.js
+++ b/tools/winscope/src/mixins/SaveAsZip.js
@@ -57,6 +57,7 @@
     },
     async downloadAsZip(traces) {
       const zip = new JSZip();
+      this.buttonClicked("Download All")
 
       for (const trace of traces) {
         const traceFolder = zip.folder(trace.type);
diff --git a/tools/winscope/src/mixins/Timeline.js b/tools/winscope/src/mixins/Timeline.js
index ec9cde9..444d525 100644
--- a/tools/winscope/src/mixins/Timeline.js
+++ b/tools/winscope/src/mixins/Timeline.js
@@ -142,10 +142,8 @@
     timelineTransitions() {
       const transitions = [];
 
-      //group tags by transition 'id' property
-      const groupedTags = _.mapValues(
-        _.groupBy(this.tags, 'id'), clist => clist.map(tag => _.omit(tag, 'id')))
-      ;
+      //group tags by transition and 'id' property
+      const groupedTags = _.groupBy(this.tags, tag => `"${tag.transition} ${tag.id}"`);
 
       for (const transitionId in groupedTags) {
         const id = groupedTags[transitionId];
@@ -154,46 +152,56 @@
         const startTimes = id.filter(tag => tag.isStartTag).map(tag => tag.timestamp);
         const endTimes = id.filter(tag => !tag.isStartTag).map(tag => tag.timestamp);
 
-        const transitionStartTime = Math.min(startTimes);
-        const transitionEndTime = Math.max(endTimes);
+        const transitionStartTime = Math.min(...startTimes);
+        const transitionEndTime = Math.max(...endTimes);
 
         //do not freeze new transition, as overlap still to be handled (defaulted to 0)
         const transition = this.generateTransition(
           transitionStartTime,
           transitionEndTime,
           id[0].transition,
-          0
+          0,
+          id[0].layerId,
+          id[0].taskId,
+          id[0].windowToken
         );
         transitions.push(transition);
       }
 
       //sort transitions in ascending start position in order to handle overlap
-      transitions.sort((a, b) => (a.startPos > b.startPos) ? 1: -1);
+      transitions.sort((a, b) => (a.startPos > b.startPos) ? 1 : -1);
 
       //compare each transition to the ones that came before
       for (let curr=0; curr<transitions.length; curr++) {
-        let overlapStore = [];
+        let processedTransitions = [];
 
         for (let prev=0; prev<curr; prev++) {
-          overlapStore.push(transitions[prev].overlap);
+          processedTransitions.push(transitions[prev]);
 
-          if (transitions[prev].startPos <= transitions[curr].startPos
-            && transitions[curr].startPos <= transitions[prev].startPos+transitions[prev].width
-            && transitions[curr].overlap === transitions[prev].overlap) {
+          if (this.isSimultaneousTransition(transitions[curr], transitions[prev])) {
             transitions[curr].overlap++;
           }
         }
 
-        if (overlapStore.length>0
-          && transitions[curr].overlap === Math.max(overlapStore)
-        ) transitions[curr].overlap++;
+        let overlapStore = processedTransitions.map(transition => transition.overlap);
+
+        if (transitions[curr].overlap === Math.max(...overlapStore)) {
+          let previousTransition = processedTransitions.find(transition => {
+            return transition.overlap===transitions[curr].overlap;
+          });
+          if (this.isSimultaneousTransition(transitions[curr], previousTransition)) {
+            transitions[curr].overlap++;
+          }
+        }
       }
 
       return Object.freeze(transitions);
     },
     errorPositions() {
       if (!this.flickerMode) return [];
-      const errorPositions = this.errors.map(error => this.position(error.timestamp));
+      const errorPositions = this.errors.map(
+        error => ({ pos: this.position(error.timestamp), ts: error.timestamp })
+      );
       return Object.freeze(errorPositions);
     },
   },
@@ -244,6 +252,12 @@
       return this.position(endTs) - this.position(startTs) + this.pointWidth;
     },
 
+    isSimultaneousTransition(currTransition, prevTransition) {
+      return prevTransition.startPos <= currTransition.startPos
+        && currTransition.startPos <= prevTransition.startPos+prevTransition.width
+        && currTransition.overlap === prevTransition.overlap;
+    },
+
     /**
      * Converts a position as a percentage of the timeline width to a timestamp.
      * @param {number} position - target position as a percentage of the
@@ -342,6 +356,16 @@
     },
 
     /**
+     * Handles the error click event.
+     * When an error in the timeline is clicked this function will update the timeline
+     * to match the error timestamp.
+     * @param {number} errorTimestamp
+     */
+    onErrorClick(errorTimestamp) {
+      this.$store.dispatch('updateTimelineTime', errorTimestamp);
+    },
+
+    /**
      * Generate a block object that can be used by the timeline SVG to render
      * a transformed block that starts at `startTs` and ends at `endTs`.
      * @param {number} startTs - The timestamp at which the block starts.
@@ -360,14 +384,24 @@
      * @param {number} endTs - The timestamp at which the transition ends.
      * @param {string} transitionType - The type of transition.
      * @param {number} overlap - The degree to which the transition overlaps with others.
+     * @param {number} layerId - Helps determine if transition is associated with SF trace.
+     * @param {number} taskId - Helps determine if transition is associated with WM trace.
+     * @param {number} windowToken - Helps determine if transition is associated with WM trace.
      * @return {Transition} A transition object transformed to the timeline's crop and
      *                 scale parameter.
      */
-    generateTransition(startTs, endTs, transitionType, overlap) {
+    generateTransition(startTs, endTs, transitionType, overlap, layerId, taskId, windowToken) {
       const transitionWidth = this.objectWidth(startTs, endTs);
       const transitionDesc = transitionMap.get(transitionType).desc;
       const transitionColor = transitionMap.get(transitionType).color;
-      const tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`;
+      var tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`;
+
+      if (layerId !== 0 && taskId === 0 && windowToken === "") {
+        tooltip += " SF only.";
+      } else if ((taskId !== 0 || windowToken !== "") && layerId === 0) {
+        tooltip += " WM only.";
+      }
+
       return new Transition(this.position(startTs), startTs, endTs, transitionWidth, transitionColor, overlap, tooltip);
     },
   },
diff --git a/tools/winscope/src/traces/SurfaceFlinger.ts b/tools/winscope/src/traces/SurfaceFlinger.ts
index b3f123c..049dc68 100644
--- a/tools/winscope/src/traces/SurfaceFlinger.ts
+++ b/tools/winscope/src/traces/SurfaceFlinger.ts
@@ -20,11 +20,14 @@
 
 export default class SurfaceFlinger extends TraceBase {
   sfTraceFile: Object;
+  tagGenerationTrace: Object;
 
   constructor(files) {
     const sfTraceFile = files[FILE_TYPES.SURFACE_FLINGER_TRACE];
+    const tagGenerationTrace = files[FILE_TYPES.SURFACE_FLINGER_TRACE].tagGenerationTrace;
     super(sfTraceFile.data, sfTraceFile.timeline, files);
 
+    this.tagGenerationTrace = tagGenerationTrace;
     this.sfTraceFile = sfTraceFile;
   }
 
diff --git a/tools/winscope/src/traces/WindowManager.ts b/tools/winscope/src/traces/WindowManager.ts
index 302eeca..eb41ad9 100644
--- a/tools/winscope/src/traces/WindowManager.ts
+++ b/tools/winscope/src/traces/WindowManager.ts
@@ -21,11 +21,14 @@
 
 export default class WindowManager extends TraceBase {
   wmTraceFile: Object;
+  tagGenerationTrace: Object;
 
   constructor(files) {
     const wmTraceFile = files[FILE_TYPES.WINDOW_MANAGER_TRACE];
+    const tagGenerationTrace = files[FILE_TYPES.WINDOW_MANAGER_TRACE].tagGenerationTrace;
     super(wmTraceFile.data, wmTraceFile.timeline, files);
 
+    this.tagGenerationTrace = tagGenerationTrace;
     this.wmTraceFile = wmTraceFile;
   }
 
diff --git a/tools/winscope/src/transform.js b/tools/winscope/src/transform.js
index 96e4785..51bedaf 100644
--- a/tools/winscope/src/transform.js
+++ b/tools/winscope/src/transform.js
@@ -15,6 +15,7 @@
  */
 
 import {DiffType} from './utils/diff.js';
+import {regExpTimestampSearch} from './utils/consts';
 
 // kind - a type used for categorization of different levels
 // name - name of the node
@@ -400,5 +401,18 @@
   return {short: 'V', long: 'visible', class: 'default'};
 }
 
+// Returns closest timestamp in timeline based on search input*/
+function getClosestTimestamp(searchInput, timeline) {
+  if (regExpTimestampSearch.test(searchInput)) {
+    var roundedTimestamp = parseInt(searchInput);
+  } else {
+    var roundedTimestamp = string_to_nanos(searchInput);
+  }
+  const closestTimestamp = timeline.reduce((prev, curr) => {
+    return Math.abs(curr-roundedTimestamp) < Math.abs(prev-roundedTimestamp) ? curr : prev;
+  });
+  return closestTimestamp;
+}
+
 // eslint-disable-next-line camelcase
-export {transform, ObjectTransformer, nanos_to_string, string_to_nanos, get_visible_chip};
+export {transform, ObjectTransformer, nanos_to_string, string_to_nanos, get_visible_chip, getClosestTimestamp};
diff --git a/tools/winscope/src/utils/consts.js b/tools/winscope/src/utils/consts.js
index e43a2c1..ca71b1f 100644
--- a/tools/winscope/src/utils/consts.js
+++ b/tools/winscope/src/utils/consts.js
@@ -34,7 +34,8 @@
 };
 
 const SEARCH_TYPE = {
-  TAG: 'Transitions and Errors',
+  TRANSITIONS: 'Transitions',
+  ERRORS: 'Errors',
   TIMESTAMP: 'Timestamp',
 };
 
@@ -51,11 +52,17 @@
   [TransitionType.ROTATION, {desc: 'Rotation', color: '#9900ffff'}],
   [TransitionType.PIP_ENTER, {desc: 'Entering PIP mode', color: '#4a86e8ff'}],
   [TransitionType.PIP_RESIZE, {desc: 'Resizing PIP mode', color: '#2b9e94ff'}],
+  [TransitionType.PIP_CLOSE, {desc: 'Closing PIP mode', color: 'rgb(57, 57, 182)'}],
   [TransitionType.PIP_EXIT, {desc: 'Exiting PIP mode', color: 'darkblue'}],
   [TransitionType.APP_LAUNCH, {desc: 'Launching app', color: '#ef6befff'}],
   [TransitionType.APP_CLOSE, {desc: 'Closing app', color: '#d10ddfff'}],
   [TransitionType.IME_APPEAR, {desc: 'IME appearing', color: '#ff9900ff'}],
   [TransitionType.IME_DISAPPEAR, {desc: 'IME disappearing', color: '#ad6800ff'}],
+  [TransitionType.APP_PAIRS_ENTER, {desc: 'Entering app pairs mode', color: 'rgb(58, 151, 39)'}],
+  [TransitionType.APP_PAIRS_EXIT, {desc: 'Exiting app pairs mode', color: 'rgb(45, 110, 32)'}],
 ])
 
-export { WebContentScriptMessageType, NAVIGATION_STYLE, SEARCH_TYPE, logLevel, transitionMap };
+//used to split timestamp search input by unit, to convert to nanoseconds
+const regExpTimestampSearch = new RegExp(/^\d+$/);
+
+export { WebContentScriptMessageType, NAVIGATION_STYLE, SEARCH_TYPE, logLevel, transitionMap, regExpTimestampSearch };
diff --git a/tools/winscope/src/utils/utils.js b/tools/winscope/src/utils/utils.js
index f8ec198..fae19f3 100644
--- a/tools/winscope/src/utils/utils.js
+++ b/tools/winscope/src/utils/utils.js
@@ -84,4 +84,21 @@
   return parts.reverse().join('');
 }
 
-export { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup, nanosToString, TimeUnits }
\ No newline at end of file
+/** Checks for match in window manager properties taskId, layerId, or windowToken,
+ * or surface flinger property id
+ */
+function isPropertyMatch(flickerItem, entryItem) {
+  return flickerItem.taskId === entryItem.taskId ||
+    (flickerItem.windowToken === entryItem.windowToken) ||
+    ((flickerItem.layerId === entryItem.layerId) && flickerItem.layerId !== 0) ||
+    flickerItem.layerId === entryItem.id;
+}
+
+export {
+  DIRECTION,
+  findLastMatchingSorted,
+  isPropertyMatch,
+  stableIdCompatibilityFixup,
+  nanosToString,
+  TimeUnits
+}
\ No newline at end of file
diff --git a/tools/winscope/yarn.lock b/tools/winscope/yarn.lock
index aafaf3b..a25b413 100644
--- a/tools/winscope/yarn.lock
+++ b/tools/winscope/yarn.lock
@@ -7694,6 +7694,11 @@
     node-fetch "^2.3.0"
     tslib "^1.9.3"
 
+vue-gtag@^1.16.1:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/vue-gtag/-/vue-gtag-1.16.1.tgz#edb2f20ab4f6c4d4d372dfecf8c1fcc8ab890181"
+  integrity sha512-5vs0pSGxdqrfXqN1Qwt0ZFXG0iTYjRMu/saddc7QIC5yp+DKgjWQRpGYVa7Pq+KbThxwzzMfo0sGi7ISa6NowA==
+
 vue-hot-reload-api@^2.3.0:
   version "2.3.4"
   resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"