1<!-- Copyright (C) 2017 The Android Open Source Project
2
3     Licensed under the Apache License, Version 2.0 (the "License");
4     you may not use this file except in compliance with the License.
5     You may obtain a copy of the License at
6
7          http://www.apache.org/licenses/LICENSE-2.0
8
9     Unless required by applicable law or agreed to in writing, software
10     distributed under the License is distributed on an "AS IS" BASIS,
11     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     See the License for the specific language governing permissions and
13     limitations under the License.
14-->
15<template>
16  <div id="app">
17    <md-app>
18      <md-app-toolbar md-tag="md-toolbar" class="top-toolbar">
19        <h1 class="md-title" style="flex: 1">{{title}}</h1>
20        <md-button
21          class="md-primary md-theme-default download-all-btn"
22          @click="generateTags()"
23          v-if="dataLoaded && canGenerateTags"
24        >Generate Tags</md-button>
25        <md-button
26          class="md-primary md-theme-default"
27          @click="downloadAsZip(files)"
28          v-if="dataLoaded"
29        >Download All</md-button>
30        <md-button
31          class="md-accent md-raised md-theme-default clear-btn"
32          style="box-shadow: none;"
33          @click="clear()"
34          v-if="dataLoaded"
35        >Clear</md-button>
36      </md-app-toolbar>
37
38      <md-app-content class="main-content" :style="mainContentStyle">
39        <section class="data-inputs" v-if="!dataLoaded">
40          <div class="input">
41            <dataadb class="adbinput" ref="adb" :store="store"
42              @dataReady="onDataReady" @statusChange="setStatus" />
43          </div>
44          <div class="input" @dragover.prevent @drop.prevent>
45            <datainput class="fileinput" ref="input" :store="store"
46              @dataReady="onDataReady" @statusChange="setStatus" />
47          </div>
48        </section>
49
50        <section class="data-view">
51          <div
52            class="data-view-container"
53            v-for="file in dataViewFiles"
54            :key="file.type"
55          >
56            <dataview
57              :ref="file.type"
58              :store="store"
59              :file="file"
60              :presentTags="Object.freeze(presentTags)"
61              :presentErrors="Object.freeze(presentErrors)"
62              :dataViewFiles="dataViewFiles"
63              @click="onDataViewFocus(file)"
64            />
65          </div>
66
67          <overlay
68            :presentTags="Object.freeze(presentTags)"
69            :presentErrors="Object.freeze(presentErrors)"
70            :store="store"
71            :ref="overlayRef"
72            :searchTypes="searchTypes"
73            v-if="dataLoaded"
74            v-on:bottom-nav-height-change="handleBottomNavHeightChange"
75          />
76        </section>
77      </md-app-content>
78    </md-app>
79  </div>
80</template>
81<script>
82import Overlay from './Overlay.vue';
83import DataView from './DataView.vue';
84import DataInput from './DataInput.vue';
85import LocalStore from './localstore.js';
86import DataAdb from './DataAdb.vue';
87import FileType from './mixins/FileType.js';
88import SaveAsZip from './mixins/SaveAsZip';
89import FocusedDataViewFinder from './mixins/FocusedDataViewFinder';
90import {DIRECTION} from './utils/utils';
91import Searchbar from './Searchbar.vue';
92import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
93import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js';
94import { TaggingEngine } from './flickerlib/common';
95
96const APP_NAME = 'Winscope';
97
98const CONTENT_BOTTOM_PADDING = 25;
99
100export default {
101  name: 'app',
102  mixins: [FileType, SaveAsZip, FocusedDataViewFinder],
103  data() {
104    return {
105      title: APP_NAME,
106      activeDataView: null,
107      // eslint-disable-next-line new-cap
108      store: LocalStore('app', {
109        flattened: false,
110        onlyVisible: false,
111        simplifyNames: true,
112        displayDefaults: true,
113        navigationStyle: NAVIGATION_STYLE.GLOBAL,
114        flickerTraceView: false,
115        showFileTypes: [],
116        isInputMode: false,
117      }),
118      overlayRef: 'overlay',
119      mainContentStyle: {
120        'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`,
121      },
122      tagFile: null,
123      presentTags: [],
124      presentErrors: [],
125      searchTypes: [SEARCH_TYPE.TIMESTAMP],
126      hasTagOrErrorTraces: false,
127    };
128  },
129  created() {
130    window.addEventListener('keydown', this.onKeyDown);
131    window.addEventListener('scroll', this.onScroll);
132    document.title = this.title;
133  },
134  destroyed() {
135    window.removeEventListener('keydown', this.onKeyDown);
136    window.removeEventListener('scroll', this.onScroll);
137  },
138
139  methods: {
140    /** Get states from either tag files or error files */
141    getUpdatedStates(files) {
142      var states = [];
143      for (const file of files) {
144        states.push(...file.data);
145      }
146      return states;
147    },
148    /** Get tags from all uploaded tag files*/
149    getUpdatedTags() {
150      if (this.tagFile === null) return [];
151      const tagStates = this.getUpdatedStates([this.tagFile]);
152      var tags = [];
153      tagStates.forEach(tagState => {
154        tagState.tags.forEach(tag => {
155          tag.timestamp = Number(tagState.timestamp);
156          // tags generated on frontend have transition.name due to kotlin enum
157          tag.transition = tag.transition.name ?? tag.transition;
158          tags.push(tag);
159        });
160      });
161      return tags;
162    },
163    /** Get tags from all uploaded error files*/
164    getUpdatedErrors() {
165      var errorStates = this.getUpdatedStates(this.errorFiles);
166      var errors = [];
167      //TODO (b/196201487) add check if errors empty
168      errorStates.forEach(errorState => {
169        errorState.errors.forEach(error => {
170          error.timestamp = Number(errorState.timestamp);
171          errors.push(error);
172        });
173      });
174      return errors;
175    },
176    /** Set flicker mode check for if there are tag/error traces uploaded*/
177    updateHasTagOrErrorTraces() {
178      return this.hasTagTrace() || this.hasErrorTrace();
179    },
180    hasTagTrace() {
181      return this.tagFile !== null;
182    },
183    hasErrorTrace() {
184      return this.errorFiles.length > 0;
185    },
186    /** Activate flicker search tab if tags/errors uploaded*/
187    updateSearchTypes() {
188      this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
189      if (this.hasTagTrace()) {
190        this.searchTypes.push(SEARCH_TYPE.TRANSITIONS);
191      }
192      if (this.hasErrorTrace()) {
193        this.searchTypes.push(SEARCH_TYPE.ERRORS);
194      }
195    },
196    /** Filter data view files by current show settings*/
197    updateShowFileTypes() {
198      this.store.showFileTypes = this.dataViewFiles
199        .filter((file) => file.show)
200        .map(file => file.type);
201    },
202    clear() {
203      this.store.showFileTypes = [];
204      this.tagFile = null;
205      this.$store.commit('clearFiles');
206      this.buttonClicked("Clear")
207    },
208    onDataViewFocus(file) {
209      this.$store.commit('setActiveFile', file);
210      this.activeDataView = file.type;
211    },
212    onKeyDown(event) {
213      event = event || window.event;
214      if (this.store.isInputMode) return false;
215      if (event.keyCode == 37 /* left */ ) {
216        this.$store.dispatch('advanceTimeline', DIRECTION.BACKWARD);
217      } else if (event.keyCode == 39 /* right */ ) {
218        this.$store.dispatch('advanceTimeline', DIRECTION.FORWARD);
219      } else if (event.keyCode == 38 /* up */ ) {
220        this.$refs[this.activeView][0].arrowUp();
221      } else if (event.keyCode == 40 /* down */ ) {
222        this.$refs[this.activeView][0].arrowDown();
223      } else {
224        return false;
225      }
226      event.preventDefault();
227      return true;
228    },
229    onDataReady(files) {
230      this.$store.dispatch('setFiles', files);
231
232      this.tagFile = this.tagFiles[0] ?? null;
233      this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
234      this.presentTags = this.getUpdatedTags();
235      this.presentErrors = this.getUpdatedErrors();
236      this.updateSearchTypes();
237      this.updateFocusedView();
238      this.updateShowFileTypes();
239    },
240    setStatus(status) {
241      if (status) {
242        this.title = status;
243      } else {
244        this.title = APP_NAME;
245      }
246    },
247    handleBottomNavHeightChange(newHeight) {
248      this.$set(
249          this.mainContentStyle,
250          'padding-bottom',
251          `${ CONTENT_BOTTOM_PADDING + newHeight }px`,
252      );
253    },
254    generateTags() {
255      // generate tag file
256      this.buttonClicked("Generate Tags");
257      const engine = new TaggingEngine(
258        this.$store.getters.tagGenerationWmTrace,
259        this.$store.getters.tagGenerationSfTrace,
260        (text) => { console.log(text) }
261      );
262      const tagTrace = engine.run();
263      const tagFile = this.generateTagFile(tagTrace);
264
265      // update tag trace in set files, update flicker mode
266      this.tagFile = tagFile;
267      this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
268      this.presentTags = this.getUpdatedTags();
269      this.presentErrors = this.getUpdatedErrors();
270      this.updateSearchTypes();
271    },
272
273    generateTagFile(tagTrace) {
274      const data = tagTrace.entries;
275      const blobUrl = URL.createObjectURL(new Blob([], {type: undefined}));
276      return dataFile(
277        "GeneratedTagTrace.winscope",
278        data.map((x) => x.timestamp),
279        data,
280        blobUrl,
281        FILE_TYPES.TAG_TRACE
282      );
283    },
284  },
285  computed: {
286    files() {
287      return this.$store.getters.sortedFiles.map(file => {
288        if (this.hasDataView(file)) {
289          file.show = true;
290        }
291        return file;
292      });
293    },
294    prettyDump() {
295      return JSON.stringify(this.dump, null, 2);
296    },
297    dataLoaded() {
298      return this.files.length > 0;
299    },
300    activeView() {
301      if (!this.activeDataView && this.files.length > 0) {
302        // eslint-disable-next-line vue/no-side-effects-in-computed-properties
303        this.activeDataView = this.files[0].type;
304      }
305      return this.activeDataView;
306    },
307    dataViewFiles() {
308      return this.files.filter((file) => this.hasDataView(file));
309    },
310    tagFiles() {
311      return this.$store.getters.tagFiles;
312    },
313    errorFiles() {
314      return this.$store.getters.errorFiles;
315    },
316    timelineFiles() {
317      return this.$store.getters.timelineFiles;
318    },
319    canGenerateTags() {
320      const fileTypes = this.dataViewFiles.map((file) => file.type);
321      return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER)
322        && fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER);
323    },
324  },
325  watch: {
326    title() {
327      document.title = this.title;
328    },
329  },
330  components: {
331    overlay: Overlay,
332    dataview: DataView,
333    datainput: DataInput,
334    dataadb: DataAdb,
335    searchbar: Searchbar,
336  },
337};
338</script>
339<style>
340@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@600&display=swap');
341
342#app .md-app-container {
343  /* Get rid of transforms which prevent fixed position from being used */
344  transform: none!important;
345  min-height: 100vh;
346}
347
348#app .top-toolbar {
349  box-shadow: none;
350  background-color: #fff;
351  background-color: var(--md-theme-default-background, #fff);
352  border-bottom: thin solid rgba(0,0,0,.12);
353  padding:  0 40px;
354}
355
356#app .top-toolbar .md-title {
357  font-family: 'Open Sans', sans-serif;
358  white-space: nowrap;
359  color: #5f6368;
360  margin: 0;
361  padding: 0;
362  font-size: 22px;
363  letter-spacing: 0;
364  font-weight: 600;
365}
366
367.data-view {
368  display: flex;
369  flex-direction: column;
370}
371
372.card-toolbar {
373  border-bottom: 1px solid rgba(0, 0, 0, .12);
374}
375
376.timeline {
377  margin: 16px;
378}
379
380.container {
381  display: flex;
382  flex-wrap: wrap;
383}
384
385.md-button {
386  margin-top: 1em
387}
388
389h1 {
390  font-weight: normal;
391}
392
393.data-inputs {
394  display: flex;
395  flex-wrap: wrap;
396  height: 100%;
397  width: 100%;
398  align-self: center;
399  /* align-items: center; */
400  align-content: center;
401  justify-content: center;
402}
403
404.data-inputs .input {
405  padding: 15px;
406  flex: 1 1 0;
407  max-width: 840px;
408  /* align-self: center; */
409}
410
411.data-inputs .input > div {
412  height: 100%;
413}
414
415.data-view-container {
416  padding: 25px 20px 0 20px;
417}
418
419.snackbar-break-words {
420  /* These are technically the same, but use both */
421  overflow-wrap: break-word;
422  word-wrap: break-word;
423  -ms-word-break: break-all;
424  word-break: break-word;
425  /* Adds a hyphen where the word breaks, if supported (No Blink) */
426  -ms-hyphens: auto;
427  -moz-hyphens: auto;
428  -webkit-hyphens: auto;
429  hyphens: auto;
430  padding: 10px 10px 10px 10px;
431}
432</style>