1// Copyright (C) 2022 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 {assertExists} from '../../../base/logging'; 16import {getErrorMessage} from '../../errors'; 17import {RECORDING_V2_FLAG} from '../../feature_flags'; 18import {AdbKeyManager} from '../auth/adb_key_manager'; 19import {RecordingError} from '../recording_error_handling'; 20import { 21 OnTargetChangeCallback, 22 RecordingTargetV2, 23 TargetFactory, 24} from '../recording_interfaces_v2'; 25import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils'; 26import {targetFactoryRegistry} from '../target_factory_registry'; 27import {AndroidWebusbTarget} from '../targets/android_webusb_target'; 28 29export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory'; 30const SERIAL_NUMBER_ISSUE = 'an invalid serial number'; 31const ADB_INTERFACE_ISSUE = 'an incompatible adb interface'; 32 33interface DeviceValidity { 34 isValid: boolean; 35 issues: string[]; 36} 37 38function createDeviceErrorMessage(device: USBDevice, issue: string): string { 39 const productName = device.productName; 40 return `USB device${productName ? ' ' + productName : ''} has ${issue}`; 41} 42 43export class AndroidWebusbTargetFactory implements TargetFactory { 44 readonly kind = ANDROID_WEBUSB_TARGET_FACTORY; 45 onTargetChange: OnTargetChangeCallback = () => {}; 46 private recordingProblems: string[] = []; 47 private targets: Map<string, AndroidWebusbTarget> = 48 new Map<string, AndroidWebusbTarget>(); 49 // AdbKeyManager should only be instantiated once, so we can use the same key 50 // for all devices. 51 private keyManager: AdbKeyManager = new AdbKeyManager(); 52 53 constructor(private usb: USB) { 54 this.init(); 55 } 56 57 getName() { 58 return 'Android WebUsb'; 59 } 60 61 listTargets(): RecordingTargetV2[] { 62 return Array.from(this.targets.values()); 63 } 64 65 listRecordingProblems(): string[] { 66 return this.recordingProblems; 67 } 68 69 async connectNewTarget(): Promise<RecordingTargetV2> { 70 let device: USBDevice; 71 try { 72 device = await this.usb.requestDevice({filters: [ADB_DEVICE_FILTER]}); 73 } catch (e) { 74 throw new RecordingError(getErrorMessage(e)); 75 } 76 77 const deviceValid = this.checkDeviceValidity(device); 78 if (!deviceValid.isValid) { 79 throw new RecordingError(deviceValid.issues.join('\n')); 80 } 81 82 const androidTarget = 83 new AndroidWebusbTarget(device, this.keyManager, this.onTargetChange); 84 this.targets.set(assertExists(device.serialNumber), androidTarget); 85 return androidTarget; 86 } 87 88 setOnTargetChange(onTargetChange: OnTargetChangeCallback) { 89 this.onTargetChange = onTargetChange; 90 } 91 92 private async init() { 93 for (const device of await this.usb.getDevices()) { 94 if (this.checkDeviceValidity(device).isValid) { 95 this.targets.set( 96 assertExists(device.serialNumber), 97 new AndroidWebusbTarget( 98 device, this.keyManager, this.onTargetChange)); 99 } 100 } 101 102 this.usb.addEventListener('connect', (ev: USBConnectionEvent) => { 103 if (this.checkDeviceValidity(ev.device).isValid) { 104 this.targets.set( 105 assertExists(ev.device.serialNumber), 106 new AndroidWebusbTarget( 107 ev.device, this.keyManager, this.onTargetChange)); 108 this.onTargetChange(); 109 } 110 }); 111 112 this.usb.addEventListener('disconnect', async (ev: USBConnectionEvent) => { 113 // We don't check device validity when disconnecting because if the device 114 // is invalid we would not have connected in the first place. 115 const serialNumber = assertExists(ev.device.serialNumber); 116 await assertExists(this.targets.get(serialNumber)) 117 .disconnect(`Device with serial ${serialNumber} was disconnected.`); 118 this.targets.delete(serialNumber); 119 this.onTargetChange(); 120 }); 121 } 122 123 private checkDeviceValidity(device: USBDevice): DeviceValidity { 124 const deviceValidity: DeviceValidity = {isValid: true, issues: []}; 125 if (!device.serialNumber) { 126 deviceValidity.issues.push( 127 createDeviceErrorMessage(device, SERIAL_NUMBER_ISSUE)); 128 deviceValidity.isValid = false; 129 } 130 if (!findInterfaceAndEndpoint(device)) { 131 deviceValidity.issues.push( 132 createDeviceErrorMessage(device, ADB_INTERFACE_ISSUE)); 133 deviceValidity.isValid = false; 134 } 135 this.recordingProblems.push(...deviceValidity.issues); 136 return deviceValidity; 137 } 138} 139 140// We only want to instantiate this class if: 141// 1. The browser implements the USB functionality. 142// 2. Recording V2 is enabled. 143if (navigator.usb && RECORDING_V2_FLAG.get()) { 144 targetFactoryRegistry.register(new AndroidWebusbTargetFactory(navigator.usb)); 145} 146