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">ADB Connect</div>
19    </md-card-header>
20    <md-card-content v-if="status === STATES.CONNECTING">
21      <md-progress-spinner md-indeterminate></md-progress-spinner>
22    </md-card-content>
23    <md-card-content v-if="status === STATES.NO_PROXY">
24      <md-icon class="md-accent">error</md-icon>
25      <span class="md-subheading">Unable to connect to Winscope ADB proxy</span>
26      <div class="md-body-2">
27        <p>Launch the Winscope ADB Connect proxy to capture traces directly from your browser.</p>
28        <p>Python 3.5+ and ADB is required.</p>
29        <p>Run:</p>
30        <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre>
31        <p>Or get it from the AOSP repository.</p>
32      </div>
33      <div class="md-layout">
34        <md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button>
35        <md-button class="md-accent" @click="restart">Retry</md-button>
36      </div>
37    </md-card-content>
38    <md-card-content v-if="status === STATES.INVALID_VERSION">
39      <md-icon class="md-accent">update</md-icon>
40      <span class="md-subheading">The version of Winscope ADB Connect proxy running on your machine is incopatibile with Winscope.</span>
41      <div class="md-body-2">
42        <p>Please update the proxy to version {{ WINSCOPE_PROXY_VERSION }}</p>
43        <p>Run:</p>
44        <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre>
45        <p>Or get it from the AOSP repository.</p>
46      </div>
47      <div class="md-layout">
48        <md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button>
49        <md-button class="md-accent" @click="restart">Retry</md-button>
50      </div>
51    </md-card-content>
52    <md-card-content v-if="status === STATES.UNAUTH">
53      <md-icon class="md-accent">lock</md-icon>
54      <span class="md-subheading">Proxy authorisation required</span>
55      <md-field>
56        <label>Enter Winscope proxy token</label>
57        <md-input v-model="adbStore.proxyKey"></md-input>
58      </md-field>
59      <div class="md-body-2">The proxy token is printed to console on proxy launch, copy and paste it above.</div>
60      <div class="md-layout">
61        <md-button class="md-primary" @click="restart">Connect</md-button>
62      </div>
63    </md-card-content>
64    <md-card-content v-if="status === STATES.DEVICES">
65      <div class="md-subheading">{{ Object.keys(devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div>
66      <md-list>
67        <md-list-item v-for="(device, id) in devices" :key="id" @click="selectDevice(id)" :disabled="!device.authorised">
68          <md-icon>{{ device.authorised ? "smartphone" : "screen_lock_portrait" }}</md-icon>
69          <span class="md-list-item-text">{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span>
70        </md-list-item>
71      </md-list>
72      <md-progress-spinner :md-size="30" md-indeterminate></md-progress-spinner>
73    </md-card-content>
74    <md-card-content v-if="status === STATES.START_TRACE">
75      <div class="device-choice">
76        <md-list>
77          <md-list-item>
78            <md-icon>smartphone</md-icon>
79            <span class="md-list-item-text">{{ devices[selectedDevice].model }} ({{ selectedDevice }})</span>
80          </md-list-item>
81        </md-list>
82        <md-button class="md-primary" @click="resetLastDevice">Change device</md-button>
83      </div>
84      <div class="trace-section">
85        <h3>Trace targets:</h3>
86        <div class="selection">
87          <md-checkbox class="md-primary" v-for="traceKey in Object.keys(TRACES)" :key="traceKey" v-model="adbStore[traceKey]">{{TRACES[traceKey].name}}</md-checkbox>
88        </div>
89        <div class="trace-config">
90            <h4>Surface Flinger config</h4>
91            <div class="selection">
92              <md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="adbStore[config]">{{config}}</md-checkbox>
93              <div class="selection">
94                <md-field class="config-selection" v-for="selectConfig in Object.keys(SF_SELECTED_CONFIG)" :key="selectConfig">
95                  <md-select v-model="SF_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig">
96                    <md-option value="">{{selectConfig}}</md-option>
97                    <md-option v-for="option in SF_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option>
98                  </md-select>
99                </md-field>
100              </div>
101            </div>
102        </div>
103        <div class="trace-config">
104            <h4>Window Manager config</h4>
105            <div class="selection">
106              <md-field class="config-selection" v-for="selectConfig in Object.keys(WM_SELECTED_CONFIG)" :key="selectConfig">
107                <md-select v-model="WM_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig">
108                  <md-option value="">{{selectConfig}}</md-option>
109                  <md-option v-for="option in WM_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option>
110                </md-select>
111              </md-field>
112            </div>
113        </div>
114        <md-button class="md-primary trace-btn" @click="startTrace">Start trace</md-button>
115      </div>
116      <div class="dump-section">
117        <h3>Dump targets:</h3>
118        <div class="selection">
119          <md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="adbStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox>
120        </div>
121        <div class="md-layout">
122          <md-button class="md-primary dump-btn" @click="dumpState">Dump state</md-button>
123        </div>
124      </div>
125    </md-card-content>
126    <md-card-content v-if="status === STATES.ERROR">
127      <md-icon class="md-accent">error</md-icon>
128      <span class="md-subheading">Error:</span>
129      <pre>
130        {{ errorText }}
131      </pre>
132      <md-button class="md-primary" @click="restart">Retry</md-button>
133    </md-card-content>
134    <md-card-content v-if="status === STATES.END_TRACE">
135      <span class="md-subheading">Tracing...</span>
136      <md-progress-bar md-mode="indeterminate"></md-progress-bar>
137      <div class="md-layout">
138        <md-button class="md-primary" @click="endTrace">End trace</md-button>
139      </div>
140    </md-card-content>
141    <md-card-content v-if="status === STATES.LOAD_DATA">
142      <span class="md-subheading">Loading data...</span>
143      <md-progress-bar md-mode="determinate" :md-value="loadProgress"></md-progress-bar>
144    </md-card-content>
145  </flat-card>
146</template>
147<script>
148import {FILE_DECODERS, FILE_TYPES} from './decode.js';
149import LocalStore from './localstore.js';
150import FlatCard from './components/FlatCard.vue';
151
152const STATES = {
153  ERROR: 0,
154  CONNECTING: 1,
155  NO_PROXY: 2,
156  INVALID_VERSION: 3,
157  UNAUTH: 4,
158  DEVICES: 5,
159  START_TRACE: 6,
160  END_TRACE: 7,
161  LOAD_DATA: 8,
162};
163
164const WINSCOPE_PROXY_VERSION = '0.8';
165const WINSCOPE_PROXY_URL = 'http://localhost:5544';
166const PROXY_ENDPOINTS = {
167  DEVICES: '/devices/',
168  START_TRACE: '/start/',
169  END_TRACE: '/end/',
170  CONFIG_TRACE: '/configtrace/',
171  SELECTED_WM_CONFIG_TRACE: '/selectedwmconfigtrace/',
172  SELECTED_SF_CONFIG_TRACE: '/selectedsfconfigtrace/',
173  DUMP: '/dump/',
174  FETCH: '/fetch/',
175  STATUS: '/status/',
176};
177
178const TRACES = {
179  'window_trace': {
180    name: 'Window Manager',
181  },
182  'accessibility_trace': {
183    name: 'Accessibility',
184  },
185  'layers_trace': {
186    name: 'Surface Flinger',
187  },
188  'transaction': {
189    name: 'Transactions',
190  },
191  'proto_log': {
192    name: 'ProtoLog',
193  },
194  'screen_recording': {
195    name: 'Screen Recording',
196  },
197  'ime_trace_clients': {
198    name: 'Input Method Clients',
199  },
200  'ime_trace_service': {
201    name: 'Input Method Service',
202  },
203  'ime_trace_managerservice': {
204    name: 'Input Method Manager Service',
205  },
206};
207
208const TRACE_CONFIG = {
209  'layers_trace': [
210    'composition',
211    'metadata',
212    'hwc',
213  ],
214};
215
216const SF_SELECTED_CONFIG = {
217  'sfbuffersize': [
218    '4000',
219    '8000',
220    '16000',
221    '32000',
222  ],
223};
224
225const WM_SELECTED_CONFIG = {
226  'wmbuffersize': [
227    '4000',
228    '8000',
229    '16000',
230    '32000',
231  ],
232  'tracingtype': [
233    'frame',
234    'transaction',
235  ],
236  'tracinglevel': [
237    'all',
238    'trim',
239    'critical',
240  ],
241};
242
243const DUMPS = {
244  'window_dump': {
245    name: 'Window Manager',
246  },
247  'layers_dump': {
248    name: 'Surface Flinger',
249  },
250};
251
252const proxyFileTypeAdapter = {
253  'window_trace': FILE_TYPES.WINDOW_MANAGER_TRACE,
254  'accessibility_trace': FILE_TYPES.ACCESSIBILITY_TRACE,
255  'layers_trace': FILE_TYPES.SURFACE_FLINGER_TRACE,
256  'wl_trace': FILE_TYPES.WAYLAND_TRACE,
257  'layers_dump': FILE_TYPES.SURFACE_FLINGER_DUMP,
258  'window_dump': FILE_TYPES.WINDOW_MANAGER_DUMP,
259  'wl_dump': FILE_TYPES.WAYLAND_DUMP,
260  'screen_recording': FILE_TYPES.SCREEN_RECORDING,
261  'transactions': FILE_TYPES.TRANSACTIONS_TRACE,
262  'proto_log': FILE_TYPES.PROTO_LOG,
263  'system_ui_trace': FILE_TYPES.SYSTEM_UI,
264  'launcher_trace': FILE_TYPES.LAUNCHER,
265  'ime_trace_clients': FILE_TYPES.IME_TRACE_CLIENTS,
266  'ime_trace_service': FILE_TYPES.IME_TRACE_SERVICE,
267  'ime_trace_managerservice': FILE_TYPES.IME_TRACE_MANAGERSERVICE,
268};
269
270const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]);
271
272export default {
273  name: 'dataadb',
274  data() {
275    return {
276      STATES,
277      TRACES,
278      TRACE_CONFIG,
279      SF_SELECTED_CONFIG,
280      WM_SELECTED_CONFIG,
281      SF_SELECTED_CONFIG_VALUES: {},
282      WM_SELECTED_CONFIG_VALUES: {},
283      DUMPS,
284      FILE_DECODERS,
285      WINSCOPE_PROXY_VERSION,
286      status: STATES.CONNECTING,
287      dataFiles: [],
288      devices: {},
289      selectedDevice: '',
290      refresh_worker: null,
291      keep_alive_worker: null,
292      errorText: '',
293      loadProgress: 0,
294      adbStore: LocalStore(
295          'adb',
296          Object.assign(
297              {
298                proxyKey: '',
299                lastDevice: '',
300              },
301              Object.keys(TRACES)
302                  .concat(Object.keys(DUMPS))
303                  .concat(CONFIGS)
304                  .reduce(function(obj, key) {
305                    obj[key] = true; return obj;
306                  }, {}),
307          ),
308      ),
309      downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py',
310    };
311  },
312  props: ['store'],
313  components: {
314    'flat-card': FlatCard,
315  },
316  methods: {
317    getDevices() {
318      if (this.status !== STATES.DEVICES && this.status !== STATES.CONNECTING) {
319        clearInterval(this.refresh_worker);
320        this.refresh_worker = null;
321        return;
322      }
323      this.callProxy('GET', PROXY_ENDPOINTS.DEVICES, this, function(request, view) {
324        try {
325          view.devices = JSON.parse(request.responseText);
326          if (view.adbStore.lastDevice && view.devices[view.adbStore.lastDevice] && view.devices[view.adbStore.lastDevice].authorised) {
327            view.selectDevice(view.adbStore.lastDevice);
328          } else {
329            if (view.refresh_worker === null) {
330              view.refresh_worker = setInterval(view.getDevices, 1000);
331            }
332            view.status = STATES.DEVICES;
333          }
334        } catch (err) {
335          console.error(err);
336          view.errorText = request.responseText;
337          view.status = STATES.ERROR;
338        }
339      });
340    },
341    keepAliveTrace() {
342      if (this.status !== STATES.END_TRACE) {
343        clearInterval(this.keep_alive_worker);
344        this.keep_alive_worker = null;
345        return;
346      }
347      this.callProxy('GET', `${PROXY_ENDPOINTS.STATUS}${this.deviceId()}/`, this, function(request, view) {
348        if (request.responseText !== 'True') {
349          view.endTrace();
350        } else if (view.keep_alive_worker === null) {
351          view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000);
352        }
353      });
354    },
355    startTrace() {
356      const requested = this.toTrace();
357      const requestedConfig = this.toTraceConfig();
358      const requestedSelectedSfConfig = this.toSelectedSfTraceConfig();
359      const requestedSelectedWmConfig = this.toSelectedWmTraceConfig();
360      if (requested.length < 1) {
361        this.errorText = 'No targets selected';
362        this.status = STATES.ERROR;
363        this.newEventOccurred("No targets selected");
364        return;
365      }
366
367      this.newEventOccurred("Start Trace");
368      this.callProxy('POST', `${PROXY_ENDPOINTS.CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedConfig);
369      this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_SF_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedSfConfig);
370      this.callProxy('POST',  `${PROXY_ENDPOINTS.SELECTED_WM_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedWmConfig);
371      this.status = STATES.END_TRACE;
372      this.callProxy('POST', `${PROXY_ENDPOINTS.START_TRACE}${this.deviceId()}/`, this, function(request, view) {
373        view.keepAliveTrace();
374      }, null, requested);
375    },
376    dumpState() {
377      this.buttonClicked("Dump State");
378      const requested = this.toDump();
379      if (requested.length < 1) {
380        this.errorText = 'No targets selected';
381        this.status = STATES.ERROR;
382        this.newEventOccurred("No targets selected");
383        return;
384      }
385      this.status = STATES.LOAD_DATA;
386      this.callProxy('POST', `${PROXY_ENDPOINTS.DUMP}${this.deviceId()}/`, this, function(request, view) {
387        view.loadFile(requested, 0);
388      }, null, requested);
389    },
390    endTrace() {
391      this.status = STATES.LOAD_DATA;
392      this.callProxy('POST', `${PROXY_ENDPOINTS.END_TRACE}${this.deviceId()}/`, this, function(request, view) {
393        view.loadFile(view.toTrace(), 0);
394      });
395      this.newEventOccurred("Ended Trace");
396    },
397    loadFile(files, idx) {
398      this.callProxy('GET', `${PROXY_ENDPOINTS.FETCH}${this.deviceId()}/${files[idx]}/`, this, function(request, view) {
399        try {
400          const enc = new TextDecoder('utf-8');
401          const resp = enc.decode(request.response);
402          const filesByType = JSON.parse(resp);
403
404          for (const filetype in filesByType) {
405            if (filesByType.hasOwnProperty(filetype)) {
406              const files = filesByType[filetype];
407              const fileDecoder = FILE_DECODERS[proxyFileTypeAdapter[filetype]];
408
409              for (const encodedFileBuffer of files) {
410                const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0));
411                const data = fileDecoder.decoder(buffer, fileDecoder.decoderParams, fileDecoder.name, view.store);
412                view.dataFiles.push(data);
413                view.loadProgress = 100 * (idx + 1) / files.length; // TODO: Update this
414              }
415            }
416          }
417
418          if (idx < files.length - 1) {
419            view.loadFile(files, idx + 1);
420          } else {
421            view.$emit('dataReady', view.dataFiles);
422          }
423        } catch (err) {
424          console.error(err);
425          view.errorText = err;
426          view.status = STATES.ERROR;
427        }
428      }, 'arraybuffer');
429    },
430    toTrace() {
431      return Object.keys(TRACES)
432          .filter((traceKey) => this.adbStore[traceKey]);
433    },
434    toTraceConfig() {
435      return Object.keys(TRACE_CONFIG)
436          .filter((file) => this.adbStore[file])
437          .flatMap((file) => TRACE_CONFIG[file])
438          .filter((config) => this.adbStore[config]);
439    },
440    toSelectedSfTraceConfig() {
441      const requestedSelectedConfig = {};
442      for (const config in this.SF_SELECTED_CONFIG_VALUES) {
443        if (this.SF_SELECTED_CONFIG_VALUES[config] !== "") {
444          requestedSelectedConfig[config] = this.SF_SELECTED_CONFIG_VALUES[config];
445        }
446      }
447      return requestedSelectedConfig;
448    },
449    toSelectedWmTraceConfig() {
450      const requestedSelectedConfig = {};
451      for (const config in this.WM_SELECTED_CONFIG_VALUES) {
452        if (this.WM_SELECTED_CONFIG_VALUES[config] !== "") {
453          requestedSelectedConfig[config] = this.WM_SELECTED_CONFIG_VALUES[config];
454        }
455      }
456      return requestedSelectedConfig;
457    },
458    toDump() {
459      return Object.keys(DUMPS)
460          .filter((dumpKey) => this.adbStore[dumpKey]);
461    },
462    selectDevice(device_id) {
463      this.selectedDevice = device_id;
464      this.adbStore.lastDevice = device_id;
465      this.status = STATES.START_TRACE;
466    },
467    deviceId() {
468      return this.selectedDevice;
469    },
470    restart() {
471      this.buttonClicked("Connect / Retry");
472      this.status = STATES.CONNECTING;
473    },
474    resetLastDevice() {
475      this.buttonClicked("Change Device");
476      this.adbStore.lastDevice = '';
477      this.restart();
478    },
479    callProxy(method, path, view, onSuccess, type, jsonRequest) {
480      const request = new XMLHttpRequest();
481      var view = this;
482      request.onreadystatechange = function() {
483        if (this.readyState !== 4) {
484          return;
485        }
486        if (this.status === 0) {
487          view.status = STATES.NO_PROXY;
488        } else if (this.status === 200) {
489          if (this.getResponseHeader('Winscope-Proxy-Version') !== WINSCOPE_PROXY_VERSION) {
490            view.status = STATES.INVALID_VERSION;
491          } else if (onSuccess) {
492            onSuccess(this, view);
493          }
494        } else if (this.status === 403) {
495          view.status = STATES.UNAUTH;
496        } else {
497          if (this.responseType === 'text' || !this.responseType) {
498            view.errorText = this.responseText;
499          } else if (this.responseType === 'arraybuffer') {
500            view.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response));
501          }
502          view.status = STATES.ERROR;
503        }
504      };
505      request.responseType = type || '';
506      request.open(method, WINSCOPE_PROXY_URL + path);
507      request.setRequestHeader('Winscope-Token', this.adbStore.proxyKey);
508      if (jsonRequest) {
509        const json = JSON.stringify(jsonRequest);
510        request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
511        request.send(json);
512      } else {
513        request.send();
514      }
515    },
516  },
517  created() {
518    const urlParams = new URLSearchParams(window.location.search);
519    if (urlParams.has('token')) {
520      this.adbStore.proxyKey = urlParams.get('token');
521    }
522    this.getDevices();
523  },
524  watch: {
525    status: {
526      handler(st) {
527        if (st == STATES.CONNECTING) {
528          this.getDevices();
529        }
530      },
531    },
532  },
533};
534
535</script>
536<style scoped>
537.config-selection {
538  width: 150px;
539  display: inline-flex;
540  margin-left: 5px;
541  margin-right: 5px;
542}
543.device-choice {
544  display: inline-flex;
545}
546h3 {
547  margin-bottom: 0;
548}
549.trace-btn, .dump-btn {
550  margin-top: 0;
551}
552pre {
553  white-space: pre-wrap;
554}
555</style>
556