| <!-- Copyright (C) 2019 The Android Open Source Project |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --> |
| <template> |
| <flat-card style="min-width: 50em"> |
| <md-card-header> |
| <div class="md-title">ADB Connect</div> |
| </md-card-header> |
| <md-card-content v-if="status === STATES.CONNECTING"> |
| <md-progress-spinner md-indeterminate></md-progress-spinner> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.NO_PROXY"> |
| <md-icon class="md-accent">error</md-icon> |
| <span class="md-subheading">Unable to connect to Winscope ADB proxy</span> |
| <div class="md-body-2"> |
| <p>Launch the Winscope ADB Connect proxy to capture traces directly from your browser.</p> |
| <p>Python 3.5+ and ADB is required.</p> |
| <p>Run:</p> |
| <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre> |
| <p>Or get it from the AOSP repository.</p> |
| </div> |
| <div class="md-layout"> |
| <md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button> |
| <md-button class="md-accent" @click="restart">Retry</md-button> |
| </div> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.INVALID_VERSION"> |
| <md-icon class="md-accent">update</md-icon> |
| <span class="md-subheading">The version of Winscope ADB Connect proxy running on your machine is incopatibile with Winscope.</span> |
| <div class="md-body-2"> |
| <p>Please update the proxy to version {{ WINSCOPE_PROXY_VERSION }}</p> |
| <p>Run:</p> |
| <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre> |
| <p>Or get it from the AOSP repository.</p> |
| </div> |
| <div class="md-layout"> |
| <md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button> |
| <md-button class="md-accent" @click="restart">Retry</md-button> |
| </div> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.UNAUTH"> |
| <md-icon class="md-accent">lock</md-icon> |
| <span class="md-subheading">Proxy authorisation required</span> |
| <md-field> |
| <label>Enter Winscope proxy token</label> |
| <md-input v-model="adbStore.proxyKey"></md-input> |
| </md-field> |
| <div class="md-body-2">The proxy token is printed to console on proxy launch, copy and paste it above.</div> |
| <div class="md-layout"> |
| <md-button class="md-primary" @click="restart">Connect</md-button> |
| </div> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.DEVICES"> |
| <div class="md-subheading">{{ Object.keys(devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div> |
| <md-list> |
| <md-list-item v-for="(device, id) in devices" :key="id" @click="selectDevice(id)" :disabled="!device.authorised"> |
| <md-icon>{{ device.authorised ? "smartphone" : "screen_lock_portrait" }}</md-icon> |
| <span class="md-list-item-text">{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span> |
| </md-list-item> |
| </md-list> |
| <md-progress-spinner :md-size="30" md-indeterminate></md-progress-spinner> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.START_TRACE"> |
| <div class="device-choice"> |
| <md-list> |
| <md-list-item> |
| <md-icon>smartphone</md-icon> |
| <span class="md-list-item-text">{{ devices[selectedDevice].model }} ({{ selectedDevice }})</span> |
| </md-list-item> |
| </md-list> |
| <md-button class="md-primary" @click="resetLastDevice">Change device</md-button> |
| </div> |
| <div class="trace-section"> |
| <h3>Trace targets:</h3> |
| <div class="selection"> |
| <md-checkbox class="md-primary" v-for="traceKey in Object.keys(TRACES)" :key="traceKey" v-model="adbStore[traceKey]">{{TRACES[traceKey].name}}</md-checkbox> |
| </div> |
| <div class="trace-config"> |
| <h4>Surface Flinger config</h4> |
| <div class="selection"> |
| <md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="adbStore[config]">{{config}}</md-checkbox> |
| <div class="selection"> |
| <md-field class="config-selection" v-for="selectConfig in Object.keys(SF_SELECTED_CONFIG)" :key="selectConfig"> |
| <md-select v-model="SF_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig"> |
| <md-option value="">{{selectConfig}}</md-option> |
| <md-option v-for="option in SF_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option> |
| </md-select> |
| </md-field> |
| </div> |
| </div> |
| </div> |
| <div class="trace-config"> |
| <h4>Window Manager config</h4> |
| <div class="selection"> |
| <md-field class="config-selection" v-for="selectConfig in Object.keys(WM_SELECTED_CONFIG)" :key="selectConfig"> |
| <md-select v-model="WM_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig"> |
| <md-option value="">{{selectConfig}}</md-option> |
| <md-option v-for="option in WM_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option> |
| </md-select> |
| </md-field> |
| </div> |
| </div> |
| <md-button class="md-primary trace-btn" @click="startTrace">Start trace</md-button> |
| </div> |
| <div class="dump-section"> |
| <h3>Dump targets:</h3> |
| <div class="selection"> |
| <md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="adbStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox> |
| </div> |
| <div class="md-layout"> |
| <md-button class="md-primary dump-btn" @click="dumpState">Dump state</md-button> |
| </div> |
| </div> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.ERROR"> |
| <md-icon class="md-accent">error</md-icon> |
| <span class="md-subheading">Error:</span> |
| <pre> |
| {{ errorText }} |
| </pre> |
| <md-button class="md-primary" @click="restart">Retry</md-button> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.END_TRACE"> |
| <span class="md-subheading">Tracing...</span> |
| <md-progress-bar md-mode="indeterminate"></md-progress-bar> |
| <div class="md-layout"> |
| <md-button class="md-primary" @click="endTrace">End trace</md-button> |
| </div> |
| </md-card-content> |
| <md-card-content v-if="status === STATES.LOAD_DATA"> |
| <span class="md-subheading">Loading data...</span> |
| <md-progress-bar md-mode="determinate" :md-value="loadProgress"></md-progress-bar> |
| </md-card-content> |
| </flat-card> |
| </template> |
| <script> |
| import {FILE_DECODERS, FILE_TYPES} from './decode.js'; |
| import LocalStore from './localstore.js'; |
| import FlatCard from './components/FlatCard.vue'; |
| |
| const STATES = { |
| ERROR: 0, |
| CONNECTING: 1, |
| NO_PROXY: 2, |
| INVALID_VERSION: 3, |
| UNAUTH: 4, |
| DEVICES: 5, |
| START_TRACE: 6, |
| END_TRACE: 7, |
| LOAD_DATA: 8, |
| }; |
| |
| const WINSCOPE_PROXY_VERSION = '0.8'; |
| const WINSCOPE_PROXY_URL = 'http://localhost:5544'; |
| const PROXY_ENDPOINTS = { |
| DEVICES: '/devices/', |
| START_TRACE: '/start/', |
| END_TRACE: '/end/', |
| CONFIG_TRACE: '/configtrace/', |
| SELECTED_WM_CONFIG_TRACE: '/selectedwmconfigtrace/', |
| SELECTED_SF_CONFIG_TRACE: '/selectedsfconfigtrace/', |
| DUMP: '/dump/', |
| FETCH: '/fetch/', |
| STATUS: '/status/', |
| }; |
| |
| const TRACES = { |
| 'window_trace': { |
| name: 'Window Manager', |
| }, |
| 'accessibility_trace': { |
| name: 'Accessibility', |
| }, |
| 'layers_trace': { |
| name: 'Surface Flinger', |
| }, |
| 'transaction': { |
| name: 'Transactions', |
| }, |
| 'proto_log': { |
| name: 'ProtoLog', |
| }, |
| 'screen_recording': { |
| name: 'Screen Recording', |
| }, |
| 'ime_trace_clients': { |
| name: 'Input Method Clients', |
| }, |
| 'ime_trace_service': { |
| name: 'Input Method Service', |
| }, |
| 'ime_trace_managerservice': { |
| name: 'Input Method Manager Service', |
| }, |
| }; |
| |
| const TRACE_CONFIG = { |
| 'layers_trace': [ |
| 'composition', |
| 'metadata', |
| 'hwc', |
| ], |
| }; |
| |
| const SF_SELECTED_CONFIG = { |
| 'sfbuffersize': [ |
| '4000', |
| '8000', |
| '16000', |
| '32000', |
| ], |
| }; |
| |
| const WM_SELECTED_CONFIG = { |
| 'wmbuffersize': [ |
| '4000', |
| '8000', |
| '16000', |
| '32000', |
| ], |
| 'tracingtype': [ |
| 'frame', |
| 'transaction', |
| ], |
| 'tracinglevel': [ |
| 'all', |
| 'trim', |
| 'critical', |
| ], |
| }; |
| |
| const DUMPS = { |
| 'window_dump': { |
| name: 'Window Manager', |
| }, |
| 'layers_dump': { |
| name: 'Surface Flinger', |
| }, |
| }; |
| |
| const proxyFileTypeAdapter = { |
| 'window_trace': FILE_TYPES.WINDOW_MANAGER_TRACE, |
| 'accessibility_trace': FILE_TYPES.ACCESSIBILITY_TRACE, |
| 'layers_trace': FILE_TYPES.SURFACE_FLINGER_TRACE, |
| 'wl_trace': FILE_TYPES.WAYLAND_TRACE, |
| 'layers_dump': FILE_TYPES.SURFACE_FLINGER_DUMP, |
| 'window_dump': FILE_TYPES.WINDOW_MANAGER_DUMP, |
| 'wl_dump': FILE_TYPES.WAYLAND_DUMP, |
| 'screen_recording': FILE_TYPES.SCREEN_RECORDING, |
| 'transactions': FILE_TYPES.TRANSACTIONS_TRACE, |
| 'proto_log': FILE_TYPES.PROTO_LOG, |
| 'system_ui_trace': FILE_TYPES.SYSTEM_UI, |
| 'launcher_trace': FILE_TYPES.LAUNCHER, |
| 'ime_trace_clients': FILE_TYPES.IME_TRACE_CLIENTS, |
| 'ime_trace_service': FILE_TYPES.IME_TRACE_SERVICE, |
| 'ime_trace_managerservice': FILE_TYPES.IME_TRACE_MANAGERSERVICE, |
| }; |
| |
| const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]); |
| |
| export default { |
| name: 'dataadb', |
| data() { |
| return { |
| STATES, |
| TRACES, |
| TRACE_CONFIG, |
| SF_SELECTED_CONFIG, |
| WM_SELECTED_CONFIG, |
| SF_SELECTED_CONFIG_VALUES: {}, |
| WM_SELECTED_CONFIG_VALUES: {}, |
| DUMPS, |
| FILE_DECODERS, |
| WINSCOPE_PROXY_VERSION, |
| status: STATES.CONNECTING, |
| dataFiles: [], |
| devices: {}, |
| selectedDevice: '', |
| refresh_worker: null, |
| keep_alive_worker: null, |
| errorText: '', |
| loadProgress: 0, |
| adbStore: LocalStore( |
| 'adb', |
| Object.assign( |
| { |
| proxyKey: '', |
| lastDevice: '', |
| }, |
| Object.keys(TRACES) |
| .concat(Object.keys(DUMPS)) |
| .concat(CONFIGS) |
| .reduce(function(obj, key) { |
| obj[key] = true; return obj; |
| }, {}), |
| ), |
| ), |
| downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py', |
| }; |
| }, |
| props: ['store'], |
| components: { |
| 'flat-card': FlatCard, |
| }, |
| methods: { |
| getDevices() { |
| if (this.status !== STATES.DEVICES && this.status !== STATES.CONNECTING) { |
| clearInterval(this.refresh_worker); |
| this.refresh_worker = null; |
| return; |
| } |
| this.callProxy('GET', PROXY_ENDPOINTS.DEVICES, this, function(request, view) { |
| try { |
| view.devices = JSON.parse(request.responseText); |
| if (view.adbStore.lastDevice && view.devices[view.adbStore.lastDevice] && view.devices[view.adbStore.lastDevice].authorised) { |
| view.selectDevice(view.adbStore.lastDevice); |
| } else { |
| if (view.refresh_worker === null) { |
| view.refresh_worker = setInterval(view.getDevices, 1000); |
| } |
| view.status = STATES.DEVICES; |
| } |
| } catch (err) { |
| console.error(err); |
| view.errorText = request.responseText; |
| view.status = STATES.ERROR; |
| } |
| }); |
| }, |
| keepAliveTrace() { |
| if (this.status !== STATES.END_TRACE) { |
| clearInterval(this.keep_alive_worker); |
| this.keep_alive_worker = null; |
| return; |
| } |
| this.callProxy('GET', `${PROXY_ENDPOINTS.STATUS}${this.deviceId()}/`, this, function(request, view) { |
| if (request.responseText !== 'True') { |
| view.endTrace(); |
| } else if (view.keep_alive_worker === null) { |
| view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000); |
| } |
| }); |
| }, |
| startTrace() { |
| const requested = this.toTrace(); |
| const requestedConfig = this.toTraceConfig(); |
| const requestedSelectedSfConfig = this.toSelectedSfTraceConfig(); |
| const requestedSelectedWmConfig = this.toSelectedWmTraceConfig(); |
| if (requested.length < 1) { |
| this.errorText = 'No targets selected'; |
| this.status = STATES.ERROR; |
| this.newEventOccurred("No targets selected"); |
| return; |
| } |
| |
| this.newEventOccurred("Start Trace"); |
| this.callProxy('POST', `${PROXY_ENDPOINTS.CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedConfig); |
| this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_SF_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedSfConfig); |
| this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_WM_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedWmConfig); |
| this.status = STATES.END_TRACE; |
| this.callProxy('POST', `${PROXY_ENDPOINTS.START_TRACE}${this.deviceId()}/`, this, function(request, view) { |
| view.keepAliveTrace(); |
| }, null, requested); |
| }, |
| dumpState() { |
| this.buttonClicked("Dump State"); |
| const requested = this.toDump(); |
| if (requested.length < 1) { |
| this.errorText = 'No targets selected'; |
| this.status = STATES.ERROR; |
| this.newEventOccurred("No targets selected"); |
| return; |
| } |
| this.status = STATES.LOAD_DATA; |
| this.callProxy('POST', `${PROXY_ENDPOINTS.DUMP}${this.deviceId()}/`, this, function(request, view) { |
| view.loadFile(requested, 0); |
| }, null, requested); |
| }, |
| endTrace() { |
| this.status = STATES.LOAD_DATA; |
| this.callProxy('POST', `${PROXY_ENDPOINTS.END_TRACE}${this.deviceId()}/`, this, function(request, view) { |
| view.loadFile(view.toTrace(), 0); |
| }); |
| this.newEventOccurred("Ended Trace"); |
| }, |
| loadFile(files, idx) { |
| this.callProxy('GET', `${PROXY_ENDPOINTS.FETCH}${this.deviceId()}/${files[idx]}/`, this, function(request, view) { |
| try { |
| const enc = new TextDecoder('utf-8'); |
| const resp = enc.decode(request.response); |
| const filesByType = JSON.parse(resp); |
| |
| for (const filetype in filesByType) { |
| if (filesByType.hasOwnProperty(filetype)) { |
| const files = filesByType[filetype]; |
| const fileDecoder = FILE_DECODERS[proxyFileTypeAdapter[filetype]]; |
| |
| for (const encodedFileBuffer of files) { |
| const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0)); |
| const data = fileDecoder.decoder(buffer, fileDecoder.decoderParams, fileDecoder.name, view.store); |
| view.dataFiles.push(data); |
| view.loadProgress = 100 * (idx + 1) / files.length; // TODO: Update this |
| } |
| } |
| } |
| |
| if (idx < files.length - 1) { |
| view.loadFile(files, idx + 1); |
| } else { |
| view.$emit('dataReady', view.dataFiles); |
| } |
| } catch (err) { |
| console.error(err); |
| view.errorText = err; |
| view.status = STATES.ERROR; |
| } |
| }, 'arraybuffer'); |
| }, |
| toTrace() { |
| return Object.keys(TRACES) |
| .filter((traceKey) => this.adbStore[traceKey]); |
| }, |
| toTraceConfig() { |
| return Object.keys(TRACE_CONFIG) |
| .filter((file) => this.adbStore[file]) |
| .flatMap((file) => TRACE_CONFIG[file]) |
| .filter((config) => this.adbStore[config]); |
| }, |
| toSelectedSfTraceConfig() { |
| const requestedSelectedConfig = {}; |
| for (const config in this.SF_SELECTED_CONFIG_VALUES) { |
| if (this.SF_SELECTED_CONFIG_VALUES[config] !== "") { |
| requestedSelectedConfig[config] = this.SF_SELECTED_CONFIG_VALUES[config]; |
| } |
| } |
| return requestedSelectedConfig; |
| }, |
| toSelectedWmTraceConfig() { |
| const requestedSelectedConfig = {}; |
| for (const config in this.WM_SELECTED_CONFIG_VALUES) { |
| if (this.WM_SELECTED_CONFIG_VALUES[config] !== "") { |
| requestedSelectedConfig[config] = this.WM_SELECTED_CONFIG_VALUES[config]; |
| } |
| } |
| return requestedSelectedConfig; |
| }, |
| toDump() { |
| return Object.keys(DUMPS) |
| .filter((dumpKey) => this.adbStore[dumpKey]); |
| }, |
| selectDevice(device_id) { |
| this.selectedDevice = device_id; |
| this.adbStore.lastDevice = device_id; |
| this.status = STATES.START_TRACE; |
| }, |
| deviceId() { |
| return this.selectedDevice; |
| }, |
| restart() { |
| this.buttonClicked("Connect / Retry"); |
| this.status = STATES.CONNECTING; |
| }, |
| resetLastDevice() { |
| this.buttonClicked("Change Device"); |
| this.adbStore.lastDevice = ''; |
| this.restart(); |
| }, |
| callProxy(method, path, view, onSuccess, type, jsonRequest) { |
| const request = new XMLHttpRequest(); |
| var view = this; |
| request.onreadystatechange = function() { |
| if (this.readyState !== 4) { |
| return; |
| } |
| if (this.status === 0) { |
| view.status = STATES.NO_PROXY; |
| } else if (this.status === 200) { |
| if (this.getResponseHeader('Winscope-Proxy-Version') !== WINSCOPE_PROXY_VERSION) { |
| view.status = STATES.INVALID_VERSION; |
| } else if (onSuccess) { |
| onSuccess(this, view); |
| } |
| } else if (this.status === 403) { |
| view.status = STATES.UNAUTH; |
| } else { |
| if (this.responseType === 'text' || !this.responseType) { |
| view.errorText = this.responseText; |
| } else if (this.responseType === 'arraybuffer') { |
| view.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response)); |
| } |
| view.status = STATES.ERROR; |
| } |
| }; |
| request.responseType = type || ''; |
| request.open(method, WINSCOPE_PROXY_URL + path); |
| request.setRequestHeader('Winscope-Token', this.adbStore.proxyKey); |
| if (jsonRequest) { |
| const json = JSON.stringify(jsonRequest); |
| request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); |
| request.send(json); |
| } else { |
| request.send(); |
| } |
| }, |
| }, |
| created() { |
| const urlParams = new URLSearchParams(window.location.search); |
| if (urlParams.has('token')) { |
| this.adbStore.proxyKey = urlParams.get('token'); |
| } |
| this.getDevices(); |
| }, |
| watch: { |
| status: { |
| handler(st) { |
| if (st == STATES.CONNECTING) { |
| this.getDevices(); |
| } |
| }, |
| }, |
| }, |
| }; |
| |
| </script> |
| <style scoped> |
| .config-selection { |
| width: 150px; |
| display: inline-flex; |
| margin-left: 5px; |
| margin-right: 5px; |
| } |
| .device-choice { |
| display: inline-flex; |
| } |
| h3 { |
| margin-bottom: 0; |
| } |
| .trace-btn, .dump-btn { |
| margin-top: 0; |
| } |
| pre { |
| white-space: pre-wrap; |
| } |
| </style> |