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