• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!-- Copyright (C) 2019 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  <flat-card style="min-width: 50em">
17    <md-card-header>
18      <div class="md-title">Open files</div>
19    </md-card-header>
20    <md-card-content>
21      <md-list>
22        <md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
23          <md-icon>{{FILE_ICONS[file.type]}}</md-icon>
24          <span class="md-list-item-text">{{file.filename}} ({{file.type}})
25          </span>
26          <md-button
27            class="md-icon-button md-accent"
28            @click="onRemoveFile(file.type)"
29          >
30            <md-icon>close</md-icon>
31          </md-button>
32        </md-list-item>
33      </md-list>
34      <md-progress-spinner
35        :md-diameter="30"
36        :md-stroke="3"
37        md-mode="indeterminate"
38        v-show="loadingFiles"
39      />
40      <div>
41        <md-checkbox v-model="store.displayDefaults" class="md-primary">
42          Show default properties
43          <md-tooltip md-direction="bottom">
44            If checked, shows the value of all properties.
45            Otherwise, hides all properties whose value is the default for its
46            data type.
47          </md-tooltip>
48        </md-checkbox>
49      </div>
50      <div class="md-layout">
51        <div class="md-layout-item md-small-size-100">
52          <md-field>
53          <md-select v-model="fileType" id="file-type" placeholder="File type">
54            <md-option value="auto">Detect type</md-option>
55            <md-option value="bugreport">Bug Report (.zip)</md-option>
56            <md-option
57              :value="k" v-for="(v,k) in FILE_DECODERS"
58              v-bind:key="v.name">{{v.name}}
59            ></md-option>
60          </md-select>
61          </md-field>
62        </div>
63      </div>
64      <div class="md-layout">
65        <input
66          type="file"
67          @change="onLoadFile"
68          ref="fileUpload"
69          v-show="false"
70          :multiple="fileType === 'auto'"
71        />
72        <md-button
73          class="md-primary md-theme-default"
74          @click="$refs.fileUpload.click()"
75        >
76          Add File
77        </md-button>
78        <md-button
79          v-if="dataReady"
80          @click="onSubmit"
81          class="md-button md-primary md-raised md-theme-default"
82        >
83          Submit
84        </md-button>
85      </div>
86    </md-card-content>
87
88    <md-snackbar
89      md-position="center"
90      :md-duration="Infinity"
91      :md-active.sync="showFetchingSnackbar"
92      md-persistent
93    >
94      <span>{{ fetchingSnackbarText }}</span>
95    </md-snackbar>
96
97    <md-snackbar
98      md-position="center"
99      :md-duration="snackbarDuration"
100      :md-active.sync="showSnackbar"
101      md-persistent
102    >
103      <span style="white-space: pre-line;">{{ snackbarText }}</span>
104      <div @click="hideSnackbarMessage()">
105        <md-button class="md-icon-button">
106          <md-icon style="color: white">close</md-icon>
107        </md-button>
108      </div>
109    </md-snackbar>
110  </flat-card>
111</template>
112<script>
113import FlatCard from './components/FlatCard.vue';
114import JSZip from 'jszip';
115import {
116  detectAndDecode,
117  FILE_TYPES,
118  FILE_DECODERS,
119  FILE_ICONS,
120  UndetectableFileType,
121} from './decode.js';
122import {WebContentScriptMessageType} from './utils/consts';
123
124export default {
125  name: 'datainput',
126  data() {
127    return {
128      FILE_TYPES,
129      FILE_DECODERS,
130      FILE_ICONS,
131      fileType: 'auto',
132      dataFiles: {},
133      loadingFiles: false,
134      showFetchingSnackbar: false,
135      showSnackbar: false,
136      snackbarDuration: 3500,
137      snackbarText: '',
138      fetchingSnackbarText: 'Fetching files...',
139    };
140  },
141  props: ['store'],
142  created() {
143    // Attempt to load files from extension if present
144    this.loadFilesFromExtension();
145  },
146  methods: {
147    showSnackbarMessage(message, duration) {
148      this.snackbarText = message;
149      this.snackbarDuration = duration;
150      this.showSnackbar = true;
151    },
152    hideSnackbarMessage() {
153      this.showSnackbar = false;
154    },
155    getFetchFilesLoadingAnimation() {
156      let frame = 0;
157      const fetchingStatusAnimation = () => {
158        frame++;
159        this.fetchingSnackbarText = `Fetching files${'.'.repeat(frame % 4)}`;
160      };
161      let interval = undefined;
162
163      return Object.freeze({
164        start: () => {
165          this.showFetchingSnackbar = true;
166          interval = setInterval(fetchingStatusAnimation, 500);
167        },
168        stop: () => {
169          this.showFetchingSnackbar = false;
170          clearInterval(interval);
171        },
172      });
173    },
174    /**
175     * Attempt to load files from the extension if present.
176     *
177     * If the source URL parameter is set to the extension it make a request
178     * to the extension to fetch the files from the extension.
179     */
180    loadFilesFromExtension() {
181      const urlParams = new URLSearchParams(window.location.search);
182      if (urlParams.get('source') === 'openFromExtension' && chrome) {
183        // Fetch files from extension
184        const androidBugToolExtensionId = 'mbbaofdfoekifkfpgehgffcpagbbjkmj';
185
186        const loading = this.getFetchFilesLoadingAnimation();
187        loading.start();
188
189        // Request to convert the blob object url "blob:chrome-extension://xxx"
190        // the chrome extension has to a web downloadable url "blob:http://xxx".
191        chrome.runtime.sendMessage(androidBugToolExtensionId, {
192          action: WebContentScriptMessageType.CONVERT_OBJECT_URL,
193        }, async (response) => {
194          switch (response.action) {
195            case WebContentScriptMessageType.CONVERT_OBJECT_URL_RESPONSE:
196              if (response.attachments?.length > 0) {
197                const filesBlobPromises = response.attachments
198                    .map(async (attachment) => {
199                      const fileQueryResponse =
200                        await fetch(attachment.objectUrl);
201                      const blob = await fileQueryResponse.blob();
202
203                      /**
204                       * Note: The blob's media type is not correct.
205                       * It is always set to "image/png".
206                       * Context: http://google3/javascript/closure/html/safeurl.js?g=0&l=256&rcl=273756987
207                       */
208
209                      // Clone blob to clear media type.
210                      const file = new Blob([blob]);
211                      file.name = attachment.name;
212
213                      return file;
214                    });
215
216                const files = await Promise.all(filesBlobPromises);
217
218                loading.stop();
219                this.processFiles(files);
220              } else {
221                const failureMessages = 'Got no attachements from extension...';
222                console.warn(failureMessages);
223                this.showSnackbarMessage(failureMessages, 3500);
224              }
225              break;
226
227            default:
228              loading.stop();
229              const failureMessages =
230                'Received unhandled response code from extension.';
231              console.warn(failureMessages);
232              this.showSnackbarMessage(failureMessages, 3500);
233          }
234        });
235      }
236    },
237    onLoadFile(e) {
238      const files = event.target.files || event.dataTransfer.files;
239      this.processFiles(files);
240    },
241    async processFiles(files) {
242      let error;
243      const decodedFiles = [];
244      for (const file of files) {
245        try {
246          this.loadingFiles = true;
247          this.showSnackbarMessage(`Loading ${file.name}`, Infinity);
248          const result = await this.addFile(file);
249          decodedFiles.push(...result);
250          this.hideSnackbarMessage();
251        } catch (e) {
252          this.showSnackbarMessage(
253              `Failed to load '${file.name}'...\n${e}`, 5000);
254          console.error(e);
255          error = e;
256          break;
257        } finally {
258          this.loadingFiles = false;
259        }
260      }
261
262      event.target.value = '';
263
264      if (error) {
265        return;
266      }
267
268      // TODO: Handle the fact that we can now have multiple files of type
269      // FILE_TYPES.TRANSACTION_EVENTS_TRACE
270
271      const decodedFileTypes = new Set(Object.keys(this.dataFiles));
272      // A file is overridden if a file of the same type is upload twice, as
273      // Winscope currently only support at most one file to each type
274      const overriddenFileTypes = new Set();
275      const overriddenFiles = {}; // filetype => array of files
276      for (const decodedFile of decodedFiles) {
277        const dataType = decodedFile.filetype;
278
279        if (decodedFileTypes.has(dataType)) {
280          overriddenFileTypes.add(dataType);
281          (overriddenFiles[dataType] = overriddenFiles[dataType] || [])
282              .push(this.dataFiles[dataType]);
283        }
284        decodedFileTypes.add(dataType);
285
286        this.$set(this.dataFiles,
287            dataType, decodedFile.data);
288      }
289
290      // TODO(b/169305853): Remove this once we have magic numbers or another
291      // way to detect the file type more reliably.
292      for (const dataType in overriddenFiles) {
293        if (overriddenFiles.hasOwnProperty(dataType)) {
294          const files = overriddenFiles[dataType];
295          files.push(this.dataFiles[dataType]);
296
297          const selectedFile =
298              this.getMostLikelyCandidateFile(dataType, files);
299          this.$set(this.dataFiles, dataType, selectedFile);
300
301          // Remove selected file from overriden list
302          const index = files.indexOf(selectedFile);
303          files.splice(index, 1);
304        }
305      }
306
307      if (overriddenFileTypes.size > 0) {
308        this.displayFilesOverridenWarning(overriddenFiles);
309      }
310    },
311
312    /**
313     * Gets the file that is most likely to be the actual file of that type out
314     * of all the candidateFiles. This is required because there are some file
315     * types that have no magic number and may lead to false positives when
316     * decoding in decode.js. (b/169305853)
317     * @param {string} dataType - The type of the candidate files.
318     * @param {files[]} candidateFiles - The list all the files detected to be
319     *                                   of type dataType, passed in the order
320     *                                   they are detected/uploaded in.
321     * @return {file} - the most likely candidate.
322     */
323    getMostLikelyCandidateFile(dataType, candidateFiles) {
324      const keyWordsByDataType = {
325        [FILE_TYPES.WINDOW_MANAGER_DUMP]: 'window',
326        [FILE_TYPES.SURFACE_FLINGER_DUMP]: 'surface',
327      };
328
329      if (
330        !candidateFiles ||
331        !candidateFiles.length ||
332        candidateFiles.length == 0
333      ) {
334        throw new Error('No candidate files provided');
335      }
336
337      if (!keyWordsByDataType.hasOwnProperty(dataType)) {
338        console.warn(`setMostLikelyCandidateFile doesn't know how to handle ` +
339            `candidates of dataType ${dataType} – setting last candidate as ` +
340            `target file.`);
341
342        // We want to return the last candidate file so that, we always override
343        // old uploaded files with once of the latest uploaded files.
344        return candidateFiles.slice(-1)[0];
345      }
346
347      for (const file of candidateFiles) {
348        if (file.filename
349            .toLowerCase().includes(keyWordsByDataType[dataType])) {
350          return file;
351        }
352      }
353
354      // We want to return the last candidate file so that, we always override
355      // old uploaded files with once of the latest uploaded files.
356      return candidateFiles.slice(-1)[0];
357    },
358
359    /**
360     * Display a snackbar warning that files have been overriden and any
361     * relavant additional information in the logs.
362     * @param {{string: file[]}} overriddenFiles - a mapping from data types to
363     * the files of the of that datatype tha have been overriden.
364     */
365    displayFilesOverridenWarning(overriddenFiles) {
366      const overriddenFileTypes = Object.keys(overriddenFiles);
367      const overriddenCount = Object.values(overriddenFiles)
368          .map((files) => files.length).reduce((length, next) => length + next);
369
370      if (overriddenFileTypes.length === 1 && overriddenCount === 1) {
371        const type = overriddenFileTypes.values().next().value;
372        const overriddenFile = overriddenFiles[type][0].filename;
373        const keptFile = this.dataFiles[type].filename;
374        const message =
375          `'${overriddenFile}' is conflicting with '${keptFile}'. ` +
376          `Only '${keptFile}' will be kept. If you wish to display ` +
377          `'${overriddenFile}', please upload it again with no other file ` +
378          `of the same type.`;
379
380        this.showSnackbarMessage(`WARNING: ${message}`, Infinity);
381        console.warn(message);
382      } else {
383        const message = `Mutiple conflicting files have been uploaded. ` +
384          `${overriddenCount} files have been discarded. Please check the ` +
385          `developer console for more information.`;
386        this.showSnackbarMessage(`WARNING: ${message}`, Infinity);
387
388        const messageBuilder = [];
389        for (const type of overriddenFileTypes.values()) {
390          const keptFile = this.dataFiles[type].filename;
391          const overriddenFilesCount = overriddenFiles[type].length;
392
393          messageBuilder.push(`${overriddenFilesCount} file` +
394              `${overriddenFilesCount > 1 ? 's' : ''} of type ${type} ` +
395              `${overriddenFilesCount > 1 ? 'have' : 'has'} been ` +
396              `overridden. Only '${keptFile}' has been kept.`);
397        }
398
399        messageBuilder.push('');
400        messageBuilder.push('Please reupload the specific files you want ' +
401          'to read (one of each type).');
402        messageBuilder.push('');
403
404        messageBuilder.push('===============DISCARDED FILES===============');
405
406        for (const type of overriddenFileTypes.values()) {
407          const discardedFiles = overriddenFiles[type];
408
409          messageBuilder.push(`The following files of type ${type} ` +
410            `have been discarded:`);
411          for (const discardedFile of discardedFiles) {
412            messageBuilder.push(`  - ${discardedFile.filename}`);
413          }
414          messageBuilder.push('');
415        }
416
417        console.warn(messageBuilder.join('\n'));
418      }
419    },
420
421    getFileExtensions(file) {
422      const split = file.name.split('.');
423      if (split.length > 1) {
424        return split.pop();
425      }
426
427      return undefined;
428    },
429    async addFile(file) {
430      const decodedFiles = [];
431      const type = this.fileType;
432
433      const extension = this.getFileExtensions(file);
434
435      // extension === 'zip' is required on top of file.type ===
436      // 'application/zip' because when loaded from the extension the type is
437      // incorrect. See comment in loadFilesFromExtension() for more
438      // information.
439      if (type === 'bugreport' ||
440          (type === 'auto' && (extension === 'zip' ||
441            file.type === 'application/zip'))) {
442        const results = await this.decodeArchive(file);
443        decodedFiles.push(...results);
444      } else {
445        const decodedFile = await this.decodeFile(file);
446        decodedFiles.push(decodedFile);
447      }
448
449      return decodedFiles;
450    },
451    readFile(file) {
452      return new Promise((resolve, _) => {
453        const reader = new FileReader();
454        reader.onload = async (e) => {
455          const buffer = new Uint8Array(e.target.result);
456          resolve(buffer);
457        };
458        reader.readAsArrayBuffer(file);
459      });
460    },
461    async decodeFile(file) {
462      const buffer = await this.readFile(file);
463
464      let filetype = this.filetype;
465      let data;
466      if (filetype) {
467        const fileDecoder = FILE_DECODERS[filetype];
468        data = fileDecoder.decoder(
469            buffer, fileDecoder.decoderParams, file.name, this.store);
470      } else {
471        // Defaulting to auto — will attempt to detect file type
472        [filetype, data] = detectAndDecode(buffer, file.name, this.store);
473      }
474
475      return {filetype, data};
476    },
477    async decodeArchive(archive) {
478      const buffer = await this.readFile(archive);
479
480      const zip = new JSZip();
481      const content = await zip.loadAsync(buffer);
482
483      const decodedFiles = [];
484
485      for (const filename in content.files) {
486        if (content.files.hasOwnProperty(filename)) {
487          const file = content.files[filename];
488          if (file.dir) {
489            // Ignore directories
490            continue;
491          }
492
493          const fileBlob = await file.async('blob');
494          // Get only filename and remove rest of path
495          fileBlob.name = filename.split('/').slice(-1).pop();
496
497          try {
498            const decodedFile = await this.decodeFile(fileBlob);
499
500            decodedFiles.push(decodedFile);
501          } catch (e) {
502            if (!(e instanceof UndetectableFileType)) {
503              throw e;
504            }
505          }
506        }
507      }
508
509      if (decodedFiles.length == 0) {
510        throw new Error('No matching files found in archive', archive);
511      }
512
513      return decodedFiles;
514    },
515    onRemoveFile(typeName) {
516      this.$delete(this.dataFiles, typeName);
517    },
518    onSubmit() {
519      this.$emit('dataReady',
520          Object.keys(this.dataFiles).map((key) => this.dataFiles[key]));
521    },
522  },
523  computed: {
524    dataReady: function() {
525      return Object.keys(this.dataFiles).length > 0;
526    },
527  },
528  components: {
529    'flat-card': FlatCard,
530  },
531};
532
533</script>
534