1/*
2 * Copyright 2017, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import Vue from 'vue'
18import Vuex from 'vuex'
19import VueMaterial from 'vue-material'
20import VueGtag from "vue-gtag";
21
22import App from './App.vue'
23import { TRACE_TYPES, DUMP_TYPES, TRACE_INFO, DUMP_INFO } from './decode.js'
24import { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup } from './utils/utils.js'
25
26import 'style-loader!css-loader!vue-material/dist/vue-material.css'
27import 'style-loader!css-loader!vue-material/dist/theme/default.css'
28
29Vue.use(Vuex)
30Vue.use(VueMaterial)
31
32// Used to determine the order in which files or displayed
33const fileOrder = {
34  [TRACE_TYPES.WINDOW_MANAGER]: 1,
35  [TRACE_TYPES.SURFACE_FLINGER]: 2,
36  [TRACE_TYPES.TRANSACTION]: 3,
37  [TRACE_TYPES.PROTO_LOG]: 4,
38  [TRACE_TYPES.IME_CLIENTS]: 5,
39  [TRACE_TYPES.IME_SERVICE]: 6,
40  [TRACE_TYPES.IME_MANAGERSERVICE]: 7,
41};
42
43function sortFiles(files) {
44  return files.sort(
45    (a, b) => (fileOrder[a.type] ?? Infinity) - (fileOrder[b.type] ?? Infinity));
46};
47
48/**
49 * Find the smallest timeline timestamp in a list of files
50 * @return undefined if not timestamp exists in the timelines of the files
51 */
52function findSmallestTimestamp(files) {
53  let timestamp = Infinity;
54  for (const file of files) {
55    if (file.timeline[0] && file.timeline[0] < timestamp) {
56      timestamp = file.timeline[0];
57    }
58  }
59
60  return timestamp === Infinity ? undefined : timestamp;
61}
62
63const store = new Vuex.Store({
64  state: {
65    currentTimestamp: 0,
66    traces: {},
67    dumps: {},
68    excludeFromTimeline: [
69      TRACE_TYPES.PROTO_LOG,
70      TRACE_TYPES.TAG,
71      TRACE_TYPES.ERROR
72    ],
73    activeFile: null,
74    focusedFile: null,
75    mergedTimeline: null,
76    navigationFilesFilter: f => true,
77    // obj -> bool, identifies whether or not an item is collapsed in a treeView
78    collapsedStateStore: {},
79  },
80  getters: {
81    collapsedStateStoreFor: (state) => (item) => {
82      if (item.stableId === undefined || item.stableId === null) {
83        console.error("Missing stable ID for item", item);
84        throw new Error("Failed to get collapse state of item — missing a stableId");
85      }
86
87      return state.collapsedStateStore[stableIdCompatibilityFixup(item)];
88    },
89    files(state) {
90      return Object.values(state.traces).concat(Object.values(state.dumps));
91    },
92    sortedFiles(state, getters) {
93      return sortFiles(getters.files);
94    },
95    timelineFiles(state, getters) {
96      return Object.values(state.traces)
97        .filter(file => !state.excludeFromTimeline.includes(file.type));
98    },
99    tagFiles(state, getters) {
100      return Object.values(state.traces)
101      .filter(file => file.type === TRACE_TYPES.TAG);
102    },
103    errorFiles(state, getters) {
104      return Object.values(state.traces)
105      .filter(file => file.type === TRACE_TYPES.ERROR);
106    },
107    sortedTimelineFiles(state, getters) {
108      return sortFiles(getters.timelineFiles);
109    },
110    video(state) {
111      return state.traces[TRACE_TYPES.SCREEN_RECORDING];
112    },
113    tagGenerationWmTrace(state, getters) {
114      return state.traces[TRACE_TYPES.WINDOW_MANAGER].tagGenerationTrace;
115    },
116    tagGenerationSfTrace(state, getters) {
117      return state.traces[TRACE_TYPES.SURFACE_FLINGER].tagGenerationTrace;
118    }
119  },
120  mutations: {
121    setCurrentTimestamp(state, timestamp) {
122      state.currentTimestamp = timestamp;
123    },
124    setFileEntryIndex(state, { type, entryIndex }) {
125      if (state.traces[type]) {
126        state.traces[type].selectedIndex = entryIndex;
127      } else {
128        throw new Error("Unexpected type — not a trace...");
129      }
130    },
131    setFiles(state, files) {
132      const filesByType = {};
133      for (const file of files) {
134        if (!filesByType[file.type]) {
135          filesByType[file.type] = [];
136        }
137        filesByType[file.type].push(file);
138      }
139
140      // TODO: Extract into smaller functions
141      const traces = {};
142      for (const traceType of Object.values(TRACE_TYPES)) {
143        const traceFiles = {};
144        const typeInfo = TRACE_INFO[traceType];
145
146        for (const traceDataFile of typeInfo.files) {
147
148          const files = filesByType[traceDataFile.type];
149
150          if (!files) {
151            continue;
152          }
153
154          if (traceDataFile.oneOf) {
155            if (files.length > 1) {
156              throw new Error(`More than one file of type ${traceDataFile.type} has been provided`);
157            }
158
159            traceFiles[traceDataFile.type] = files[0];
160          } else if (traceDataFile.manyOf) {
161            traceFiles[traceDataFile.type] = files;
162          } else {
163            throw new Error("Missing oneOf or manyOf property...");
164          }
165        }
166
167        if (Object.keys(traceFiles).length > 0 && typeInfo.constructor) {
168          traces[traceType] = new typeInfo.constructor(traceFiles);
169        }
170      }
171
172      state.traces = traces;
173
174      // TODO: Refactor common code out
175      const dumps = {};
176      for (const dumpType of Object.values(DUMP_TYPES)) {
177        const dumpFiles = {};
178        const typeInfo = DUMP_INFO[dumpType];
179
180        for (const dumpDataFile of typeInfo.files) {
181          const files = filesByType[dumpDataFile.type];
182
183          if (!files) {
184            continue;
185          }
186
187          if (dumpDataFile.oneOf) {
188            if (files.length > 1) {
189              throw new Error(`More than one file of type ${dumpDataFile.type} has been provided`);
190            }
191
192            dumpFiles[dumpDataFile.type] = files[0];
193          } else if (dumpDataFile.manyOf) {
194
195          } else {
196            throw new Error("Missing oneOf or manyOf property...");
197          }
198        }
199
200        if (Object.keys(dumpFiles).length > 0 && typeInfo.constructor) {
201          dumps[dumpType] = new typeInfo.constructor(dumpFiles);
202        }
203
204      }
205
206      state.dumps = dumps;
207
208      if (!state.activeFile && Object.keys(traces).length > 0) {
209        state.activeFile = sortFiles(Object.values(traces))[0];
210      }
211
212      // TODO: Add same for dumps
213    },
214    clearFiles(state) {
215      for (const traceType in state.traces) {
216        if (state.traces.hasOwnProperty(traceType)) {
217          Vue.delete(state.traces, traceType);
218        }
219      }
220
221      for (const dumpType in state.dumps) {
222        if (state.dumps.hasOwnProperty(dumpType)) {
223          Vue.delete(state.dumps, dumpType);
224        }
225      }
226
227      state.activeFile = null;
228      state.mergedTimeline = null;
229    },
230    setActiveFile(state, file) {
231      state.activeFile = file;
232    },
233    setMergedTimeline(state, timeline) {
234      state.mergedTimeline = timeline;
235    },
236    removeMergedTimeline(state, timeline) {
237      state.mergedTimeline = null;
238    },
239    setMergedTimelineIndex(state, newIndex) {
240      state.mergedTimeline.selectedIndex = newIndex;
241    },
242    setCollapsedState(state, { item, isCollapsed }) {
243      if (item.stableId === undefined || item.stableId === null) {
244        return;
245      }
246
247      Vue.set(
248        state.collapsedStateStore,
249        stableIdCompatibilityFixup(item),
250        isCollapsed
251      );
252    },
253    setFocusedFile(state, file) {
254      state.focusedFile = file;
255    },
256    setNavigationFilesFilter(state, filter) {
257      state.navigationFilesFilter = filter;
258    },
259  },
260  actions: {
261    setFiles(context, files) {
262      context.commit('clearFiles');
263      context.commit('setFiles', files);
264
265      const timestamp = findSmallestTimestamp(files);
266      if (timestamp !== undefined) {
267        context.commit('setCurrentTimestamp', timestamp);
268      }
269    },
270    updateTimelineTime(context, timestamp) {
271      for (const file of context.getters.files) {
272        //dumps do not have a timeline, so only look at files with timelines to update the timestamp
273        if (!file.timeline) continue;
274
275        const type = file.type;
276        const entryIndex = findLastMatchingSorted(
277          file.timeline,
278          (array, idx) => parseInt(array[idx]) <= timestamp,
279        );
280
281        context.commit('setFileEntryIndex', { type, entryIndex });
282      }
283
284      if (context.state.mergedTimeline) {
285        const newIndex = findLastMatchingSorted(
286          context.state.mergedTimeline.timeline,
287          (array, idx) => parseInt(array[idx]) <= timestamp,
288        );
289
290        context.commit('setMergedTimelineIndex', newIndex);
291      }
292
293      context.commit('setCurrentTimestamp', timestamp);
294    },
295    advanceTimeline(context, direction) {
296      // NOTE: MergedTimeline is never considered to find the next closest index
297      // MergedTimeline only represented the timelines overlapped together and
298      // isn't considered an actual timeline.
299
300      if (direction !== DIRECTION.FORWARD && direction !== DIRECTION.BACKWARD) {
301        throw new Error("Unsupported direction provided.");
302      }
303
304      const consideredFiles = context.getters.timelineFiles
305        .filter(context.state.navigationFilesFilter);
306
307      let fileIndex = -1;
308      let timelineIndex;
309      let minTimeDiff = Infinity;
310
311      for (let idx = 0; idx < consideredFiles.length; idx++) {
312        const file = consideredFiles[idx];
313
314        let candidateTimestampIndex = file.selectedIndex;
315        let candidateTimestamp = file.timeline[candidateTimestampIndex];
316
317        let candidateCondition;
318        switch (direction) {
319          case DIRECTION.BACKWARD:
320            candidateCondition = () => candidateTimestamp < context.state.currentTimestamp;
321            break;
322          case DIRECTION.FORWARD:
323            candidateCondition = () => candidateTimestamp > context.state.currentTimestamp;
324            break;
325        }
326
327        if (!candidateCondition()) {
328          // Not a candidate — find a valid candidate
329          let noCandidate = false;
330          while (!candidateCondition()) {
331            candidateTimestampIndex += direction;
332            if (candidateTimestampIndex < 0 || candidateTimestampIndex >= file.timeline.length) {
333              noCandidate = true;
334              break;
335            }
336            candidateTimestamp = file.timeline[candidateTimestampIndex];
337          }
338
339          if (noCandidate) {
340            continue;
341          }
342        }
343
344        const timeDiff = Math.abs(candidateTimestamp - context.state.currentTimestamp);
345        if (minTimeDiff > timeDiff) {
346          minTimeDiff = timeDiff;
347          fileIndex = idx;
348          timelineIndex = candidateTimestampIndex;
349        }
350      }
351
352      if (fileIndex >= 0) {
353        const closestFile = consideredFiles[fileIndex];
354        const timestamp = parseInt(closestFile.timeline[timelineIndex]);
355
356        context.dispatch('updateTimelineTime', timestamp);
357      }
358    }
359  }
360})
361
362/**
363 * Make Google analytics functionalities available for recording events.
364 */
365Vue.use(VueGtag, {
366  config: { id: 'G-RRV0M08Y76'}
367})
368
369Vue.mixin({
370  methods: {
371    buttonClicked(button) {
372      const string = "Clicked " + button + " Button";
373      this.$gtag.event(string, {
374        'event_category': 'Button Clicked',
375        'event_label': "Winscope Interactions",
376        'value': button,
377      });
378    },
379    draggedAndDropped(val) {
380      this.$gtag.event("Dragged And DroppedFile", {
381        'event_category': 'Uploaded file',
382        'event_label': "Winscope Interactions",
383        'value': val,
384      });
385    },
386    uploadedFileThroughFilesystem(val) {
387      this.$gtag.event("Uploaded File From Filesystem", {
388        'event_category': 'Uploaded file',
389        'event_label': "Winscope Interactions",
390        'value': val,
391      });
392    },
393    newEventOccurred(event) {
394      this.$gtag.event(event, {
395        'event_category': event,
396        'event_label': "Winscope Interactions",
397        'value': 1,
398      });
399    },
400    seeingNewScreen(screenname) {
401      this.$gtag.screenview({
402        app_name: "Winscope",
403        screen_name: screenname,
404      })
405    },
406    openedToSeeAttributeField(field) {
407      const string = "Opened field " + field;
408      this.$gtag.event(string, {
409        'event_category': "Opened attribute field",
410        'event_label': "Winscope Interactions",
411        'value': field,
412      });
413    },
414  }
415});
416
417new Vue({
418  el: '#app',
419  store, // inject the Vuex store into all components
420  render: h => h(App)
421})
422