blob: d81006b36052a98800480dca4e4f150b1505bb7e [file] [log] [blame]
<!-- 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>
<md-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-spinner md-indeterminate></md-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-gutter">
<md-button class="md-accent md-raised" :href="downloadProxyUrl">Download from AOSP</md-button>
<md-button class="md-raised 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-gutter">
<md-button class="md-accent md-raised" :href="downloadProxyUrl">Download from AOSP</md-button>
<md-button class="md-raised 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-input-container>
<label>Enter Winscope proxy token</label>
<md-input v-model="adbStore.proxyKey"></md-input>
</md-input-container>
<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-gutter">
<md-button class="md-accent md-raised" @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>{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span>
</md-list-item>
</md-list>
<md-spinner :md-size="30" md-indeterminate></md-spinner>
</md-card-content>
<md-card-content v-if="status === STATES.START_TRACE">
<md-list>
<md-list-item>
<md-icon>smartphone</md-icon><span>{{ devices[selectedDevice].model }} ({{ selectedDevice }})</span>
</md-list-item>
</md-list>
<md-checkbox v-for="file in CAPTURE_FILES" :key="file" v-model="adbStore[file]">{{FILE_TYPES[file].name}}</md-checkbox>
<div class="md-layout md-gutter">
<md-button class="md-accent md-raised" @click="startTrace">Start trace</md-button>
<md-button class="md-raised md-accent" @click="resetLastDevice">Device list</md-button>
</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-raised md-accent" @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 md-indeterminate></md-progress>
<div class="md-layout md-gutter">
<md-button class="md-accent md-raised" @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 :md-progress="loadProgress"></md-progress>
</md-card-content>
</md-card>
</template>
<script>
import { dataFile, FILE_TYPES, DATA_TYPES } from './decode.js'
import LocalStore from './localstore.js'
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.2"
const WINSCOPE_PROXY_URL = "http://localhost:5544"
const PROXY_ENDPOINTS = {
DEVICES: "/devices/",
START_TRACE: "/start/",
END_TRACE: "/end/",
FETCH: "/fetch/",
STATUS: "/status/",
}
const CAPTURE_FILES = [
"window_trace",
"layers_trace",
"screen_recording",
"transaction"
]
export default {
name: 'dataadb',
data() {
return {
STATES,
CAPTURE_FILES,
FILE_TYPES,
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: '',
}, CAPTURE_FILES.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"],
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) {
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.toLoad()
if (requested.length < 1) {
this.errorText = "No targets selected";
this.status = STATES.ERROR;
return
}
this.callProxy("POST", PROXY_ENDPOINTS.START_TRACE + this.deviceId() + "/", this, function(request, view) {
view.status = STATES.END_TRACE;
view.keepAliveTrace();
}, null, requested)
},
endTrace() {
this.status = STATES.LOAD_DATA;
this.callProxy("POST", PROXY_ENDPOINTS.END_TRACE + this.deviceId() + "/", this, function(request, view) {
view.loadFile(view.toLoad(), 0);
})
},
loadFile(files, idx) {
this.callProxy("GET", PROXY_ENDPOINTS.FETCH + this.deviceId() + "/" + files[idx] + "/", this, function(request, view) {
try {
var buffer = new Uint8Array(request.response);
var filetype = FILE_TYPES[files[idx]];
var data = filetype.decoder(buffer, filetype, filetype.name, view.store);
view.dataFiles.push(data)
view.loadProgress = 100 * (idx + 1) / files.length;
if (idx < files.length - 1) {
view.loadFile(files, idx + 1)
} else {
view.$emit('dataReady', view.dataFiles);
}
} catch (err) {
view.errorText = err;
view.status = STATES.ERROR;
}
}, "arraybuffer")
},
toLoad() {
return CAPTURE_FILES.filter(file => this.adbStore[file]);
},
selectDevice(device_id) {
this.selectedDevice = device_id;
this.adbStore.lastDevice = device_id;
this.status = STATES.START_TRACE;
},
deviceId() {
return this.selectedDevice;
},
restart() {
this.status = STATES.CONNECTING;
},
resetLastDevice() {
this.adbStore.lastDevice = '';
this.restart()
},
callProxy(method, path, view, onSuccess, type, jsonRequest) {
var 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 {
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() {
var 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>