• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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