blob: e498d63f4fb081694b75f6c99cad4b3075800736 [file] [log] [blame]
// Copyright (C) 2022 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.
import {assertExists} from '../../../base/logging';
import {getErrorMessage} from '../../errors';
import {RECORDING_V2_FLAG} from '../../feature_flags';
import {AdbKeyManager} from '../auth/adb_key_manager';
import {RecordingError} from '../recording_error_handling';
import {
OnTargetChangeCallback,
RecordingTargetV2,
TargetFactory,
} from '../recording_interfaces_v2';
import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils';
import {targetFactoryRegistry} from '../target_factory_registry';
import {AndroidWebusbTarget} from '../targets/android_webusb_target';
export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory';
const SERIAL_NUMBER_ISSUE = 'an invalid serial number';
const ADB_INTERFACE_ISSUE = 'an incompatible adb interface';
interface DeviceValidity {
isValid: boolean;
issues: string[];
}
function createDeviceErrorMessage(device: USBDevice, issue: string): string {
const productName = device.productName;
return `USB device${productName ? ' ' + productName : ''} has ${issue}`;
}
export class AndroidWebusbTargetFactory implements TargetFactory {
readonly kind = ANDROID_WEBUSB_TARGET_FACTORY;
onTargetChange: OnTargetChangeCallback = () => {};
private recordingProblems: string[] = [];
private targets: Map<string, AndroidWebusbTarget> =
new Map<string, AndroidWebusbTarget>();
// AdbKeyManager should only be instantiated once, so we can use the same key
// for all devices.
private keyManager: AdbKeyManager = new AdbKeyManager();
constructor(private usb: USB) {
this.init();
}
getName() {
return 'Android WebUsb';
}
listTargets(): RecordingTargetV2[] {
return Array.from(this.targets.values());
}
listRecordingProblems(): string[] {
return this.recordingProblems;
}
async connectNewTarget(): Promise<RecordingTargetV2> {
let device: USBDevice;
try {
device = await this.usb.requestDevice({filters: [ADB_DEVICE_FILTER]});
} catch (e) {
throw new RecordingError(getErrorMessage(e));
}
const deviceValid = this.checkDeviceValidity(device);
if (!deviceValid.isValid) {
throw new RecordingError(deviceValid.issues.join('\n'));
}
const androidTarget =
new AndroidWebusbTarget(device, this.keyManager, this.onTargetChange);
this.targets.set(assertExists(device.serialNumber), androidTarget);
return androidTarget;
}
setOnTargetChange(onTargetChange: OnTargetChangeCallback) {
this.onTargetChange = onTargetChange;
}
private async init() {
for (const device of await this.usb.getDevices()) {
if (this.checkDeviceValidity(device).isValid) {
this.targets.set(
assertExists(device.serialNumber),
new AndroidWebusbTarget(
device, this.keyManager, this.onTargetChange));
}
}
this.usb.addEventListener('connect', (ev: USBConnectionEvent) => {
if (this.checkDeviceValidity(ev.device).isValid) {
this.targets.set(
assertExists(ev.device.serialNumber),
new AndroidWebusbTarget(
ev.device, this.keyManager, this.onTargetChange));
this.onTargetChange();
}
});
this.usb.addEventListener('disconnect', async (ev: USBConnectionEvent) => {
// We don't check device validity when disconnecting because if the device
// is invalid we would not have connected in the first place.
const serialNumber = assertExists(ev.device.serialNumber);
await assertExists(this.targets.get(serialNumber))
.disconnect(`Device with serial ${serialNumber} was disconnected.`);
this.targets.delete(serialNumber);
this.onTargetChange();
});
}
private checkDeviceValidity(device: USBDevice): DeviceValidity {
const deviceValidity: DeviceValidity = {isValid: true, issues: []};
if (!device.serialNumber) {
deviceValidity.issues.push(
createDeviceErrorMessage(device, SERIAL_NUMBER_ISSUE));
deviceValidity.isValid = false;
}
if (!findInterfaceAndEndpoint(device)) {
deviceValidity.issues.push(
createDeviceErrorMessage(device, ADB_INTERFACE_ISSUE));
deviceValidity.isValid = false;
}
this.recordingProblems.push(...deviceValidity.issues);
return deviceValidity;
}
}
// We only want to instantiate this class if:
// 1. The browser implements the USB functionality.
// 2. Recording V2 is enabled.
if (navigator.usb && RECORDING_V2_FLAG.get()) {
targetFactoryRegistry.register(new AndroidWebusbTargetFactory(navigator.usb));
}