Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 51f3729b authored by Simon Chan's avatar Simon Chan
Browse files

feat(webusb): separate static methods to a new class

make it easier to use with other WebUSB compatible implementations.
parent d08a6891
Loading
Loading
Loading
Loading
+8 −5
Original line number Diff line number Diff line
@@ -10,7 +10,8 @@ import {
} from "@fluentui/react";
import { Adb, AdbBackend, AdbPacketData, AdbPacketInit } from "@yume-chan/adb";
import AdbDirectSocketsBackend from "@yume-chan/adb-backend-direct-sockets";
import AdbWebUsbBackend, {
import {
    AdbWebUsbBackendManager,
    AdbWebUsbBackendWatcher,
} from "@yume-chan/adb-backend-webusb";
import AdbWsBackend from "@yume-chan/adb-backend-ws";
@@ -40,7 +41,8 @@ function _Connect(): JSX.Element | null {

    const [usbBackendList, setUsbBackendList] = useState<AdbBackend[]>([]);
    const updateUsbBackendList = useCallback(async () => {
        const backendList: AdbBackend[] = await AdbWebUsbBackend.getDevices();
        const backendList: AdbBackend[] =
            await AdbWebUsbBackendManager.BROWSER!.getDevices();
        setUsbBackendList(backendList);
        return backendList;
    }, []);
@@ -48,7 +50,7 @@ function _Connect(): JSX.Element | null {
    useEffect(
        () => {
            // Only run on client
            const supported = AdbWebUsbBackend.isSupported();
            const supported = !!AdbWebUsbBackendManager.BROWSER;
            setSupported(supported);

            if (!supported) {
@@ -70,7 +72,8 @@ function _Connect(): JSX.Element | null {
                        );
                        return;
                    }
                }
                },
                window.navigator.usb
            );

            return () => watcher.dispose();
@@ -165,7 +168,7 @@ function _Connect(): JSX.Element | null {
    };

    const addUsbBackend = useCallback(async () => {
        const backend = await AdbWebUsbBackend.requestDevice();
        const backend = await AdbWebUsbBackendManager.BROWSER!.requestDevice();
        setSelectedBackend(backend);
        await updateUsbBackendList();
    }, [updateUsbBackendList]);
+51 −25
Original line number Diff line number Diff line
@@ -2,15 +2,18 @@

Backend for `@yume-chan/adb` using WebUSB ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/USB), [Spec](https://wicg.github.io/webusb)) API.

-   [Note](#note)
-   [Requirements](#requirements)
-   [Use in Node.js](#use-in-nodejs)
-   [API](#api)
    -   [Constructor](#constructor)
    -   [`isSupported()`](#issupported)
    -   [`requestDevice`](#requestdevice)
-   [`AdbWebUsbBackend`](#adbwebusbbackend)
    -   [constructor](#constructor)
    -   [`connect`](#connect)
-   [`AdbWebUsbBackendManager`](#adbwebusbbackendmanager)
    -   [`BROWSER`](#browser)
    -   [constructor](#constructor-1)
    -   [`requestDevice`](#requestdevice)
    -   [`getDevices`](#getdevices)

## Note
## Requirements

WebUSB API requires [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS).

@@ -23,19 +26,19 @@ Chrome will treat `localhost` as secure, but if you want to access a dev server

## Use in Node.js

Node.js doesn't have native support for WebUSB API. However, all methods in this package have a `usbManager` parameter, which can be used to provide a WebUSB compatible implementation.
Node.js doesn't have native support for WebUSB API, but the [`usb`](https://www.npmjs.com/package/usb) NPM package has a `webusb` export that provides a WebUSB compatible API.

For example, the [`usb`](https://www.npmjs.com/package/usb) NPM package has a `webusb` export that can be used here.
The constructors of `AdbWebUsbBackend`, `AdbWebUsbBackendManager` and `AdbWebUsbBackendWatcher` all have a `usb` parameter that can be used to specify the WebUSB API implementation.

## API
## `AdbWebUsbBackend`

### Constructor
### constructor

```ts
public constructor(
    device: USBDevice,
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
    usbManager: USB = window.navigator.usb
    usb: USB
);
```

@@ -45,37 +48,60 @@ Create a new instance of `AdbWebBackend` using a `USBDevice` instance you alread

The `filters` parameter specifies the `classCode`, `subclassCode` and `protocolCode` to use when searching for ADB interface. The default value is `[{ classCode: 0xff, subclassCode: 0x42, protocolCode: 0x1 }]`, defined by Google.

### `isSupported()`
### `connect`

```ts
public async connect(): Promise<
    ReadableWritablePair<AdbPacketData, AdbPacketInit>
>
```

Claim the device and create a pair of `AdbPacket` streams to the ADB interface.

## `AdbWebUsbBackendManager`

A helper class that wraps the WebUSB API.

### `BROWSER`

```ts
public static isSupported(): boolean;
public static readonly BROWSER: AdbWebUsbBackendManager | undefined;
```

Check if WebUSB API is supported by the browser.
Gets the instance of AdbWebUsbBackendManager using browser WebUSB implementation.

May be `undefined` if the browser does not support WebUSB.

### constructor

```ts
public constructor(usb: USB);
```

Create a new instance of `AdbWebUsbBackendManager` using the specified WebUSB API implementation.

### `requestDevice`

```ts
public static async requestDevice(
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
    usbManager: USB = window.navigator.usb
public async requestDevice(
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
): Promise<AdbWebUsbBackend | undefined>
```

Request access to a connected device from browser. The browser will display a list of devices to the user and let them choose one.

Only available in browsers that support WebUSB API (When `isSupported()` returns `true`).
Request access to a connected device.
This is a convince method for `usb.requestDevice()`.

The `filters` parameter must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface. It can also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.

Returns an `AdbWebUsbBackend` instance, or `undefined` if the user cancelled the picker.

### `connect`
### `getDevices`

```ts
public async connect(): Promise<
    ReadableWritablePair<AdbPacketData, AdbPacketInit>
>
public async getDevices(
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
): Promise<AdbWebUsbBackend[]>
```

Claim the device and create a pair of `AdbPacket` streams to the ADB interface.
Get all connected and authenticated devices.
This is a convince method for `usb.getDevices()`.
+5 −76
Original line number Diff line number Diff line
@@ -130,7 +130,7 @@ export class AdbWebUsbBackendStream
        device: USBDevice,
        inEndpoint: USBEndpoint,
        outEndpoint: USBEndpoint,
        usbManager = window.navigator.usb
        usbManager: USB
    ) {
        let closed = false;

@@ -232,79 +232,8 @@ export class AdbWebUsbBackendStream
}

export class AdbWebUsbBackend implements AdbBackend {
    /**
     * Check if WebUSB API is supported by the browser.
     *
     * @returns `true` if WebUSB is supported by the current browser.
     */
    public static isSupported(): boolean {
        return !!globalThis.navigator?.usb;
    }

    /**
     * Request access to a connected device.
     * This is a convince method for `usb.requestDevice()`.
     * @param filters
     * The filters to apply to the device list.
     *
     * It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @param usbManager
     * A WebUSB compatible interface.
     * For example, `usb` NPM package for Node.js has a `webusb` object that can be used here.
     *
     * Defaults to `window.navigator.usb` (will throw an error if not exist).
     * @returns The `AdbWebUsbBackend` instance if the user selected a device,
     * or `undefined` if the user cancelled the device picker.
     */
    public static async requestDevice(
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
        usbManager = window.navigator.usb
    ): Promise<AdbWebUsbBackend | undefined> {
        try {
            const device = await usbManager.requestDevice({
                filters,
            });
            return new AdbWebUsbBackend(device, filters, usbManager);
        } catch (e) {
            // User cancelled the device picker
            if (e instanceof DOMException && e.name === "NotFoundError") {
                return undefined;
            }

            throw e;
        }
    }

    /**
     * Get all connected and authenticated devices.
     * This is a convince method for `usb.getDevices()`.
     * @param filters
     * The filters to apply to the device list.
     *
     * It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @param usbManager
     * A WebUSB compatible interface.
     * For example, `usb` NPM package for Node.js has a `webusb` object that can be used here.
     *
     * Defaults to `window.navigator.usb` (will throw an error if not exist).
     * @returns An array of `AdbWebUsbBackend` instances for all connected and authenticated devices.
     */
    public static async getDevices(
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
        usbManager = window.navigator.usb
    ): Promise<AdbWebUsbBackend[]> {
        const devices = await usbManager.getDevices();
        return devices.map((device) => new AdbWebUsbBackend(device, filters));
    }

    private _filters: AdbDeviceFilter[];
    private _usbManager: USB;
    private _usb: USB;

    private _device: USBDevice;
    public get device() {
@@ -328,11 +257,11 @@ export class AdbWebUsbBackend implements AdbBackend {
    public constructor(
        device: USBDevice,
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
        usbManager = window.navigator.usb
        usb: USB
    ) {
        this._device = device;
        this._filters = filters;
        this._usbManager = usbManager;
        this._usb = usb;
    }

    /**
@@ -380,7 +309,7 @@ export class AdbWebUsbBackend implements AdbBackend {
            this._device,
            inEndpoint,
            outEndpoint,
            this._usbManager
            this._usb
        );
    }
}
+1 −1
Original line number Diff line number Diff line
export * from "./backend.js";
export { AdbWebUsbBackend as default } from "./backend.js";
export * from "./manager.js";
export * from "./watcher.js";
+90 −0
Original line number Diff line number Diff line
import type { AdbDeviceFilter } from "./backend.js";
import { ADB_DEFAULT_DEVICE_FILTER, AdbWebUsbBackend } from "./backend.js";

export class AdbWebUsbBackendManager {
    /**
     * Gets the instance of AdbWebUsbBackendManager using browser WebUSB implementation.
     *
     * May be `undefined` if the browser does not support WebUSB.
     */
    public static readonly BROWSER =
        typeof window !== "undefined" && !!window.navigator.usb
            ? new AdbWebUsbBackendManager(window.navigator.usb)
            : undefined;

    private _usb: USB;

    /**
     * Create a new instance of `AdbWebUsbBackendManager` using the specified WebUSB API implementation.
     * @param usb A WebUSB compatible interface.
     */
    public constructor(usb: USB) {
        this._usb = usb;
    }

    /**
     * Request access to a connected device.
     * This is a convince method for `usb.requestDevice()`.
     * @param filters
     * The filters to apply to the device list.
     *
     * It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @param usbManager
     * A WebUSB compatible interface.
     * For example, `usb` NPM package for Node.js has a `webusb` object that can be used here.
     *
     * Defaults to `window.navigator.usb` (will throw an error if not exist).
     * @returns The `AdbWebUsbBackend` instance if the user selected a device,
     * or `undefined` if the user cancelled the device picker.
     */
    public async requestDevice(
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
    ): Promise<AdbWebUsbBackend | undefined> {
        try {
            const device = await this._usb.requestDevice({
                filters,
            });
            return new AdbWebUsbBackend(device, filters, this._usb);
        } catch (e) {
            // User cancelled the device picker
            // TODO: investigate what error the `usb` NPM package will throw
            if (
                typeof DOMException !== "undefined" &&
                e instanceof DOMException &&
                e.name === "NotFoundError"
            ) {
                return undefined;
            }

            throw e;
        }
    }
    /**
     * Get all connected and authenticated devices.
     * This is a convince method for `usb.getDevices()`.
     * @param filters
     * The filters to apply to the device list.
     *
     * It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @param usbManager
     * A WebUSB compatible interface.
     * For example, `usb` NPM package for Node.js has a `webusb` object that can be used here.
     *
     * Defaults to `window.navigator.usb` (will throw an error if not exist).
     * @returns An array of `AdbWebUsbBackend` instances for all connected and authenticated devices.
     */
    public async getDevices(
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
    ): Promise<AdbWebUsbBackend[]> {
        const devices = await this._usb.getDevices();
        return devices.map(
            (device) => new AdbWebUsbBackend(device, filters, this._usb)
        );
    }
}
Loading