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  <md-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-spinner md-indeterminate></md-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 md-gutter">
34        <md-button class="md-accent md-raised" :href="downloadProxyUrl">Download from AOSP</md-button>
35        <md-button class="md-raised 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 md-gutter">
48        <md-button class="md-accent md-raised" :href="downloadProxyUrl">Download from AOSP</md-button>
49        <md-button class="md-raised 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-input-container>
56        <label>Enter Winscope proxy token</label>
57        <md-input v-model="adbStore.proxyKey"></md-input>
58      </md-input-container>
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 md-gutter">
61        <md-button class="md-accent md-raised" @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><span>{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span>
69        </md-list-item>
70      </md-list>
71      <md-spinner :md-size="30" md-indeterminate></md-spinner>
72    </md-card-content>
73    <md-card-content v-if="status === STATES.START_TRACE">
74      <md-list>
75        <md-list-item>
76          <md-icon>smartphone</md-icon><span>{{ devices[selectedDevice].model }} ({{ selectedDevice }})</span>
77        </md-list-item>
78      </md-list>
79      <div>
80        <p>Trace targets:</p>
81        <md-checkbox v-for="file in TRACE_FILES" :key="file" v-model="adbStore[file]">{{FILE_TYPES[file].name}}</md-checkbox>
82      </div>
83      <div>
84        <p>Dump targets:</p>
85        <md-checkbox v-for="file in DUMP_FILES" :key="file" v-model="adbStore[file]">{{FILE_TYPES[file].name}}</md-checkbox>
86      </div>
87      <div class="md-layout md-gutter">
88        <md-button class="md-accent md-raised" @click="startTrace">Start trace</md-button>
89        <md-button class="md-accent md-raised" @click="dumpState">Dump state</md-button>
90        <md-button class="md-raised" @click="resetLastDevice">Device list</md-button>
91      </div>
92    </md-card-content>
93    <md-card-content v-if="status === STATES.ERROR">
94      <md-icon class="md-accent">error</md-icon>
95      <span class="md-subheading">Error:</span>
96      <pre>
97        {{ errorText }}
98      </pre>
99      <md-button class="md-raised md-accent" @click="restart">Retry</md-button>
100    </md-card-content>
101    <md-card-content v-if="status === STATES.END_TRACE">
102      <span class="md-subheading">Tracing...</span>
103      <md-progress md-indeterminate></md-progress>
104      <div class="md-layout md-gutter">
105        <md-button class="md-accent md-raised" @click="endTrace">End trace</md-button>
106      </div>
107    </md-card-content>
108    <md-card-content v-if="status === STATES.LOAD_DATA">
109      <span class="md-subheading">Loading data...</span>
110      <md-progress :md-progress="loadProgress"></md-progress>
111    </md-card-content>
112  </md-card>
113</template>
114<script>
115import { FILE_TYPES, DATA_TYPES } from './decode.js'
116import LocalStore from './localstore.js'
117
118const STATES = {
119  ERROR: 0,
120  CONNECTING: 1,
121  NO_PROXY: 2,
122  INVALID_VERSION: 3,
123  UNAUTH: 4,
124  DEVICES: 5,
125  START_TRACE: 6,
126  END_TRACE: 7,
127  LOAD_DATA: 8,
128}
129
130const WINSCOPE_PROXY_VERSION = "0.5"
131const WINSCOPE_PROXY_URL = "http://localhost:5544"
132const PROXY_ENDPOINTS = {
133  DEVICES: "/devices/",
134  START_TRACE: "/start/",
135  END_TRACE: "/end/",
136  DUMP: "/dump/",
137  FETCH: "/fetch/",
138  STATUS: "/status/",
139}
140const TRACE_FILES = [
141  "window_trace",
142  "layers_trace",
143  "screen_recording",
144  "transaction",
145  "proto_log"
146]
147const DUMP_FILES = [
148  "window_dump",
149  "layers_dump"
150]
151const CAPTURE_FILES = TRACE_FILES.concat(DUMP_FILES)
152
153export default {
154  name: 'dataadb',
155  data() {
156    return {
157      STATES,
158      TRACE_FILES,
159      DUMP_FILES,
160      CAPTURE_FILES,
161      FILE_TYPES,
162      WINSCOPE_PROXY_VERSION,
163      status: STATES.CONNECTING,
164      dataFiles: [],
165      devices: {},
166      selectedDevice: '',
167      refresh_worker: null,
168      keep_alive_worker: null,
169      errorText: '',
170      loadProgress: 0,
171      adbStore: LocalStore('adb', Object.assign({
172        proxyKey: '',
173        lastDevice: '',
174      }, CAPTURE_FILES.reduce(function(obj, key) { obj[key] = true; return obj }, {}))),
175      downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py',
176    }
177  },
178  props: ["store"],
179  methods: {
180    getDevices() {
181      if (this.status !== STATES.DEVICES && this.status !== STATES.CONNECTING) {
182        clearInterval(this.refresh_worker);
183        this.refresh_worker = null;
184        return;
185      }
186      this.callProxy("GET", PROXY_ENDPOINTS.DEVICES, this, function(request, view) {
187        try {
188          view.devices = JSON.parse(request.responseText);
189          if (view.adbStore.lastDevice && view.devices[view.adbStore.lastDevice] && view.devices[view.adbStore.lastDevice].authorised) {
190            view.selectDevice(view.adbStore.lastDevice)
191          } else {
192            if (view.refresh_worker === null) {
193              view.refresh_worker = setInterval(view.getDevices, 1000)
194            }
195            view.status = STATES.DEVICES;
196          }
197        } catch (err) {
198          view.errorText = request.responseText;
199          view.status = STATES.ERROR;
200        }
201      })
202    },
203    keepAliveTrace() {
204      if (this.status !== STATES.END_TRACE) {
205        clearInterval(this.keep_alive_worker);
206        this.keep_alive_worker = null;
207        return;
208      }
209      this.callProxy("GET", PROXY_ENDPOINTS.STATUS + this.deviceId() + "/", this, function(request, view) {
210        if (request.responseText !== "True") {
211          view.endTrace();
212        } else if (view.keep_alive_worker === null) {
213          view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000)
214        }
215      })
216    },
217    startTrace() {
218      const requested = this.toTrace()
219      if (requested.length < 1) {
220        this.errorText = "No targets selected";
221        this.status = STATES.ERROR;
222        return
223      }
224      this.status = STATES.END_TRACE;
225      this.callProxy("POST", PROXY_ENDPOINTS.START_TRACE + this.deviceId() + "/", this, function(request, view) {
226        view.keepAliveTrace();
227      }, null, requested)
228    },
229    dumpState() {
230      const requested = this.toDump()
231      if (requested.length < 1) {
232        this.errorText = "No targets selected";
233        this.status = STATES.ERROR;
234        return
235      }
236      this.status = STATES.LOAD_DATA;
237      this.callProxy("POST", PROXY_ENDPOINTS.DUMP + this.deviceId() + "/", this, function(request, view) {
238        view.loadFile(requested, 0);
239      }, null, requested)
240    },
241    endTrace() {
242      this.status = STATES.LOAD_DATA;
243      this.callProxy("POST", PROXY_ENDPOINTS.END_TRACE + this.deviceId() + "/", this, function(request, view) {
244        view.loadFile(view.toTrace(), 0);
245      })
246    },
247    loadFile(files, idx) {
248      this.callProxy("GET", PROXY_ENDPOINTS.FETCH + this.deviceId() + "/" + files[idx] + "/", this, function(request, view) {
249        try {
250          var buffer = new Uint8Array(request.response);
251          var filetype = FILE_TYPES[files[idx]];
252          var data = filetype.decoder(buffer, filetype, filetype.name, view.store);
253          view.dataFiles.push(data)
254          view.loadProgress = 100 * (idx + 1) / files.length;
255          if (idx < files.length - 1) {
256            view.loadFile(files, idx + 1)
257          } else {
258            view.$emit('dataReady', view.dataFiles);
259          }
260        } catch (err) {
261          view.errorText = err;
262          view.status = STATES.ERROR;
263        }
264      }, "arraybuffer")
265    },
266    toTrace() {
267      return TRACE_FILES.filter(file => this.adbStore[file]);
268    },
269    toDump() {
270      return DUMP_FILES.filter(file => this.adbStore[file]);
271    },
272    selectDevice(device_id) {
273      this.selectedDevice = device_id;
274      this.adbStore.lastDevice = device_id;
275      this.status = STATES.START_TRACE;
276    },
277    deviceId() {
278      return this.selectedDevice;
279    },
280    restart() {
281      this.status = STATES.CONNECTING;
282    },
283    resetLastDevice() {
284      this.adbStore.lastDevice = '';
285      this.restart()
286    },
287    callProxy(method, path, view, onSuccess, type, jsonRequest) {
288      var request = new XMLHttpRequest();
289      var view = this;
290      request.onreadystatechange = function() {
291        if (this.readyState !== 4) {
292          return;
293        }
294        if (this.status === 0) {
295          view.status = STATES.NO_PROXY;
296        } else if (this.status === 200) {
297          if (this.getResponseHeader("Winscope-Proxy-Version") !== WINSCOPE_PROXY_VERSION) {
298            view.status = STATES.INVALID_VERSION;
299          } else {
300            onSuccess(this, view)
301          }
302        } else if (this.status === 403) {
303          view.status = STATES.UNAUTH;
304        } else {
305          if (this.responseType === "text" || !this.responseType) {
306            view.errorText = this.responseText;
307          } else if (this.responseType === "arraybuffer") {
308            view.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response));
309          }
310          view.status = STATES.ERROR;
311        }
312      }
313      request.responseType = type || "";
314      request.open(method, WINSCOPE_PROXY_URL + path);
315      request.setRequestHeader("Winscope-Token", this.adbStore.proxyKey);
316      if (jsonRequest) {
317        const json = JSON.stringify(jsonRequest)
318        request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
319        request.send(json)
320      } else {
321        request.send();
322      }
323    }
324  },
325  created() {
326    var urlParams = new URLSearchParams(window.location.search);
327    if (urlParams.has("token")) {
328      this.adbStore.proxyKey = urlParams.get("token")
329    }
330    this.getDevices();
331  },
332  watch: {
333    status: {
334      handler(st) {
335        if (st == STATES.CONNECTING) {
336          this.getDevices();
337        }
338      }
339    }
340  },
341}
342
343</script>
344