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