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 15import {exists} from '../base/utils'; 16import {isAdbTarget} from '../common/state'; 17import { 18 extractDurationFromTraceConfig, 19 extractTraceConfig, 20} from '../core/trace_config_utils'; 21import {globals} from '../frontend/globals'; 22 23import {Adb} from './adb_interfaces'; 24import {ReadBuffersResponse} from './consumer_port_types'; 25import {Consumer, RpcConsumerPort} from './record_controller_interfaces'; 26 27export enum AdbConnectionState { 28 READY_TO_CONNECT, 29 AUTH_IN_PROGRESS, 30 CONNECTED, 31 CLOSED, 32} 33 34interface Command { 35 method: string; 36 params: Uint8Array; 37} 38 39export abstract class AdbBaseConsumerPort extends RpcConsumerPort { 40 // Contains the commands sent while the authentication is in progress. They 41 // will all be executed afterwards. If the device disconnects, they are 42 // removed. 43 private commandQueue: Command[] = []; 44 45 protected adb: Adb; 46 protected state = AdbConnectionState.READY_TO_CONNECT; 47 protected device?: USBDevice; 48 49 protected constructor(adb: Adb, consumer: Consumer) { 50 super(consumer); 51 this.adb = adb; 52 } 53 54 async handleCommand(method: string, params: Uint8Array) { 55 try { 56 if (method === 'FreeBuffers') { 57 // When we finish tracing, we disconnect the adb device interface. 58 // Otherwise, we will keep holding the device interface and won't allow 59 // adb to access it. https://wicg.github.io/webusb/#abusing-a-device 60 // "Lastly, since USB devices are unable to distinguish requests from 61 // multiple sources, operating systems only allow a USB interface to 62 // have a single owning user-space or kernel-space driver." 63 this.state = AdbConnectionState.CLOSED; 64 await this.adb.disconnect(); 65 } else if (method === 'EnableTracing') { 66 this.state = AdbConnectionState.READY_TO_CONNECT; 67 } 68 69 if (this.state === AdbConnectionState.CLOSED) return; 70 71 this.commandQueue.push({method, params}); 72 73 if ( 74 this.state === AdbConnectionState.READY_TO_CONNECT || 75 this.deviceDisconnected() 76 ) { 77 this.state = AdbConnectionState.AUTH_IN_PROGRESS; 78 this.device = await this.findDevice(); 79 if (!this.device) { 80 this.state = AdbConnectionState.READY_TO_CONNECT; 81 const target = globals.state.recordingTarget; 82 throw Error( 83 `Device with serial ${ 84 isAdbTarget(target) ? target.serial : 'n/a' 85 } not found.`, 86 ); 87 } 88 89 this.sendStatus(`Please allow USB debugging on device. 90 If you press cancel, reload the page.`); 91 92 await this.adb.connect(this.device); 93 94 // During the authentication the device may have been disconnected. 95 if (!globals.state.recordingInProgress || this.deviceDisconnected()) { 96 throw Error('Recording not in progress after adb authorization.'); 97 } 98 99 this.state = AdbConnectionState.CONNECTED; 100 this.sendStatus('Device connected.'); 101 } 102 103 if (this.state === AdbConnectionState.AUTH_IN_PROGRESS) return; 104 105 console.assert(this.state === AdbConnectionState.CONNECTED); 106 107 for (const cmd of this.commandQueue) this.invoke(cmd.method, cmd.params); 108 109 this.commandQueue = []; 110 } catch (e) { 111 this.commandQueue = []; 112 this.state = AdbConnectionState.READY_TO_CONNECT; 113 this.sendErrorMessage(e.message); 114 } 115 } 116 117 private deviceDisconnected() { 118 return !this.device || !this.device.opened; 119 } 120 121 setDurationStatus(enableTracingProto: Uint8Array) { 122 const traceConfigProto = extractTraceConfig(enableTracingProto); 123 if (!traceConfigProto) return; 124 const duration = extractDurationFromTraceConfig(traceConfigProto); 125 this.sendStatus( 126 `Recording in progress${ 127 exists(duration) ? ' for ' + duration.toString() + ' ms' : '' 128 }...`, 129 ); 130 } 131 132 abstract invoke(method: string, argsProto: Uint8Array): void; 133 134 generateChunkReadResponse( 135 data: Uint8Array, 136 last = false, 137 ): ReadBuffersResponse { 138 return { 139 type: 'ReadBuffersResponse', 140 slices: [{data, lastSliceForPacket: last}], 141 }; 142 } 143 144 async findDevice(): Promise<USBDevice | undefined> { 145 if (!('usb' in navigator)) return undefined; 146 const connectedDevice = globals.state.recordingTarget; 147 if (!isAdbTarget(connectedDevice)) return undefined; 148 const devices = await navigator.usb.getDevices(); 149 return devices.find((d) => d.serialNumber === connectedDevice.serial); 150 } 151} 152