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

Unverified Commit b87d76ca authored by Simon Chan's avatar Simon Chan
Browse files

refactor: rename `AdbDaemonConnection` to `AdbDaemonDevice`

parent a0407dc0
Loading
Loading
Loading
Loading
+15 −16
Original line number Diff line number Diff line
import type { AdbDaemonConnection } from "@yume-chan/adb";
import type { AdbDaemonDevice } from "@yume-chan/adb";
import { AdbPacket, AdbPacketSerializeStream } from "@yume-chan/adb";
import type { ReadableStream, WritableStream } from "@yume-chan/stream-extra";
import {
@@ -20,13 +20,7 @@ declare global {
        localPort: number;
    }

    class TCPSocket {
        constructor(
            remoteAddress: string,
            remotePort: number,
            options?: TCPSocketOptions
        );

    interface TCPSocket {
        opened: Promise<TCPSocketOpenInfo>;
        closed: Promise<void>;

@@ -41,16 +35,19 @@ declare global {
        keepAliveDelay?: number;
    }

    interface Window {
        TCPSocket: typeof TCPSocket;
    }
    // eslint-disable-next-line no-var
    var TCPSocket: {
        new (
            remoteAddress: string,
            remotePort: number,
            options?: TCPSocketOptions
        ): TCPSocket;
    };
}

export default class AdbDaemonDirectSocketsConnection
    implements AdbDaemonConnection
{
export default class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice {
    public static isSupported(): boolean {
        return typeof window !== "undefined" && !!window.TCPSocket;
        return typeof globalThis.TCPSocket !== "undefined";
    }

    public readonly serial: string;
@@ -69,7 +66,9 @@ export default class AdbDaemonDirectSocketsConnection
    }

    public async connect() {
        const socket = new TCPSocket(this.host, this.port, { noDelay: true });
        const socket = new globalThis.TCPSocket(this.host, this.port, {
            noDelay: true,
        });
        const { readable, writable } = await socket.opened;

        return {
+20 −20
Original line number Diff line number Diff line
# @yume-chan/adb-daemon-webusb

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

-   [Use in browser](#use-in-browser)
-   [Use in Node.js](#use-in-nodejs)
-   [`AdbDaemonWebUsbConnection`](#adbdaemonwebusbconnection)
-   [`AdbDaemonWebUsbDevice`](#adbdaemonwebusbdevice)
    -   [constructor](#constructor)
    -   [`device`](#device)
    -   [`raw`](#raw)
    -   [`connect`](#connect)
-   [`AdbDaemonWebUsbConnectionManager`](#adbdaemonwebusbconnectionmanager)
-   [`AdbDaemonWebUsbDeviceManager`](#adbdaemonwebusbdevicemanager)
    -   [`BROWSER`](#browser)
    -   [constructor](#constructor-1)
    -   [`requestDevice`](#requestdevice)
@@ -31,9 +31,9 @@ ADB daemon transport connection for `@yume-chan/adb` using WebUSB ([MDN](https:/

Node.js doesn't have native support for WebUSB API, but the [`usb`](https://www.npmjs.com/package/usb) NPM package provides a WebUSB compatible API.

To use a custom WebUSB API implementation, pass it to the constructor of `AdbDaemonWebUsbConnection`, `AdbDaemonWebUsbConnectionManager` and `AdbDaemonWebUsbConnectionWatcher` via the `usb` parameter.
To use a custom WebUSB API implementation, pass it to the constructor of `AdbDaemonWebUsbDevice`, `AdbDaemonWebUsbDeviceManager` and `AdbDaemonWebUsbConnectionWatcher` via the `usbManager` parameter.

## `AdbDaemonWebUsbConnection`
## `AdbDaemonWebUsbDevice`

### constructor

@@ -41,62 +41,62 @@ To use a custom WebUSB API implementation, pass it to the constructor of `AdbDae
public constructor(
    device: USBDevice,
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
    usb: USB
    usbManager: USB
);
```

Create a new instance of `AdbDaemonWebUsbConnection` using a specified `USBDevice` instance.
Create a new instance of `AdbDaemonWebUsbDevice` using a specified `USBDevice` instance.

`USBDevice` and `USB` types are from WebUSB API.

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.

### `device`
### `raw`

```ts
public get device(): USBDevice;
public get raw(): USBDevice;
```

Gets the `USBDevice` from the connection. Allow sending/receiving USB packets to other interfaces/endpoints. For example can be used with `@yume-chan/aoa` package.
Gets the raw `USBDevice` from the device. Allow sending/receiving USB packets to other interfaces/endpoints. For example can be used with `@yume-chan/aoa` package.

### `connect`

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

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

## `AdbDaemonWebUsbConnectionManager`
## `AdbDaemonWebUsbDeviceManager`

A helper class that wraps the WebUSB API.

### `BROWSER`

```ts
public static readonly BROWSER: AdbDaemonWebUsbConnectionManager | undefined;
public static readonly BROWSER: AdbDaemonWebUsbDeviceManager | undefined;
```

Gets the instance of `AdbDaemonWebUsbConnectionManager` using browser WebUSB implementation.
Gets the instance of `AdbDaemonWebUsbDeviceManager` using browser WebUSB implementation.

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

### constructor

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

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

### `requestDevice`

```ts
public async requestDevice(
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
): Promise<AdbDaemonWebUsbConnection | undefined>
): Promise<AdbDaemonWebUsbDevice | undefined>
```

Request access to a connected device.
@@ -104,14 +104,14 @@ 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 `AdbDaemonWebUsbConnection` instance, or `undefined` if the user cancelled the picker.
Returns an `AdbDaemonWebUsbDevice` instance, or `undefined` if the user cancelled the picker.

### `getDevices`

```ts
public async getDevices(
    filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
): Promise<AdbDaemonWebUsbConnection[]>
): Promise<AdbDaemonWebUsbDevice[]>
```

Get all connected and authenticated devices.
+42 −82
Original line number Diff line number Diff line
import type {
    AdbDaemonConnection,
    AdbDaemonDevice,
    AdbPacketData,
    AdbPacketInit,
} from "@yume-chan/adb";
@@ -22,14 +22,8 @@ import {
import type { ExactReadable } from "@yume-chan/struct";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";

/**
 * `classCode`, `subclassCode` and `protocolCode` are required
 * for selecting correct USB configuration and interface.
 */
export type AdbDeviceFilter = USBDeviceFilter &
    Required<
        Pick<USBDeviceFilter, "classCode" | "subclassCode" | "protocolCode">
    >;
import type { AdbDeviceFilter } from "./utils.js";
import { findUsbAlternateInterface, isErrorName } from "./utils.js";

/**
 * The default filter for ADB devices, as defined by Google.
@@ -40,35 +34,6 @@ export const ADB_DEFAULT_DEVICE_FILTER = {
    protocolCode: 1,
} as const satisfies AdbDeviceFilter;

function alternateMatchesFilter(
    alternate: USBAlternateInterface,
    filters: AdbDeviceFilter[]
) {
    return filters.some(
        (filter) =>
            alternate.interfaceClass === filter.classCode &&
            alternate.interfaceSubclass === filter.subclassCode &&
            alternate.interfaceProtocol === filter.protocolCode
    );
}

function findUsbAlternateInterface(
    device: USBDevice,
    filters: AdbDeviceFilter[]
) {
    for (const configuration of device.configurations) {
        for (const interface_ of configuration.interfaces) {
            for (const alternate of interface_.alternates) {
                if (alternateMatchesFilter(alternate, filters)) {
                    return { configuration, interface_, alternate };
                }
            }
        }
    }

    throw new Error("No matched alternate interface found");
}

/**
 * Find the first pair of input and output endpoints from an alternate interface.
 *
@@ -109,39 +74,39 @@ function findUsbEndpoints(endpoints: USBEndpoint[]) {
}

class Uint8ArrayExactReadable implements ExactReadable {
    private _data: Uint8Array;
    private _position: number;
    #data: Uint8Array;
    #position: number;

    public get position() {
        return this._position;
        return this.#position;
    }

    public constructor(data: Uint8Array) {
        this._data = data;
        this._position = 0;
        this.#data = data;
        this.#position = 0;
    }

    public readExactly(length: number): Uint8Array {
        const result = this._data.subarray(
            this._position,
            this._position + length
        const result = this.#data.subarray(
            this.#position,
            this.#position + length
        );
        this._position += length;
        this.#position += length;
        return result;
    }
}

export class AdbDaemonWebUsbConnectionStreams
export class AdbDaemonWebUsbConnection
    implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
{
    private _readable: ReadableStream<AdbPacketData>;
    #readable: ReadableStream<AdbPacketData>;
    public get readable() {
        return this._readable;
        return this.#readable;
    }

    private _writable: WritableStream<Consumable<AdbPacketInit>>;
    #writable: WritableStream<Consumable<AdbPacketInit>>;
    public get writable() {
        return this._writable;
        return this.#writable;
    }

    public constructor(
@@ -181,7 +146,7 @@ export class AdbDaemonWebUsbConnectionStreams

        usbManager.addEventListener("disconnect", handleUsbDisconnect);

        this._readable = duplex.wrapReadable(
        this.#readable = duplex.wrapReadable(
            new ReadableStream<AdbPacketData>({
                async pull(controller) {
                    try {
@@ -221,12 +186,7 @@ export class AdbDaemonWebUsbConnectionStreams
                        // even before the `disconnect` event is fired.
                        // We need to wait a little bit and check if the device is still connected.
                        // https://github.com/WICG/webusb/issues/219
                        if (
                            typeof e === "object" &&
                            e !== null &&
                            "name" in e &&
                            e.name === "NetworkError"
                        ) {
                        if (isErrorName(e, "NetworkError")) {
                            await new Promise<void>((resolve) => {
                                setTimeout(() => {
                                    resolve();
@@ -247,7 +207,7 @@ export class AdbDaemonWebUsbConnectionStreams
        );

        const zeroMask = outEndpoint.packetSize - 1;
        this._writable = pipeFrom(
        this.#writable = pipeFrom(
            duplex.createWritable(
                new ConsumableWritableStream({
                    write: async (chunk) => {
@@ -284,21 +244,21 @@ export class AdbDaemonWebUsbConnectionStreams
    }
}

export class AdbDaemonWebUsbConnection implements AdbDaemonConnection {
    private _filters: AdbDeviceFilter[];
    private _usb: USB;
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
    #filters: AdbDeviceFilter[];
    #usbManager: USB;

    private _device: USBDevice;
    public get device() {
        return this._device;
    #raw: USBDevice;
    public get raw() {
        return this.#raw;
    }

    public get serial(): string {
        return this._device.serialNumber!;
        return this.#raw.serialNumber!;
    }

    public get name(): string {
        return this._device.productName!;
        return this.#raw.productName!;
    }

    /**
@@ -310,11 +270,11 @@ export class AdbDaemonWebUsbConnection implements AdbDaemonConnection {
    public constructor(
        device: USBDevice,
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
        usb: USB
        usbManager: USB
    ) {
        this._device = device;
        this._filters = filters;
        this._usb = usb;
        this.#raw = device;
        this.#filters = filters;
        this.#usbManager = usbManager;
    }

    /**
@@ -324,32 +284,32 @@ export class AdbDaemonWebUsbConnection implements AdbDaemonConnection {
    public async connect(): Promise<
        ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
    > {
        if (!this._device.opened) {
            await this._device.open();
        if (!this.#raw.opened) {
            await this.#raw.open();
        }

        const { configuration, interface_, alternate } =
            findUsbAlternateInterface(this._device, this._filters);
            findUsbAlternateInterface(this.#raw, this.#filters);

        if (
            this._device.configuration?.configurationValue !==
            this.#raw.configuration?.configurationValue !==
            configuration.configurationValue
        ) {
            // Note: Switching configuration is not supported on Windows,
            // but Android devices should always expose ADB function at the first (default) configuration.
            await this._device.selectConfiguration(
            await this.#raw.selectConfiguration(
                configuration.configurationValue
            );
        }

        if (!interface_.claimed) {
            await this._device.claimInterface(interface_.interfaceNumber);
            await this.#raw.claimInterface(interface_.interfaceNumber);
        }

        if (
            interface_.alternate.alternateSetting !== alternate.alternateSetting
        ) {
            await this._device.selectAlternateInterface(
            await this.#raw.selectAlternateInterface(
                interface_.interfaceNumber,
                alternate.alternateSetting
            );
@@ -358,11 +318,11 @@ export class AdbDaemonWebUsbConnection implements AdbDaemonConnection {
        const { inEndpoint, outEndpoint } = findUsbEndpoints(
            alternate.endpoints
        );
        return new AdbDaemonWebUsbConnectionStreams(
            this._device,
        return new AdbDaemonWebUsbConnection(
            this.#raw,
            inEndpoint,
            outEndpoint,
            this._usb
            this.#usbManager
        );
    }
}
+2 −1
Original line number Diff line number Diff line
export * from "./connection.js";
export * from "./device.js";
export * from "./manager.js";
export * from "./utils.js";
export * from "./watcher.js";
+56 −34
Original line number Diff line number Diff line
import type { AdbDeviceFilter } from "./connection.js";
import {
    ADB_DEFAULT_DEVICE_FILTER,
    AdbDaemonWebUsbConnection,
} from "./connection.js";
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from "./device.js";
import type { AdbDeviceFilter } from "./utils.js";
import { findUsbAlternateInterface, isErrorName } from "./utils.js";

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

    private _usb: USB;
    #usbManager: USB;

    /**
     * Create a new instance of {@link AdbDaemonWebUsbConnectionManager} using the specified WebUSB implementation.
     * @param usb A WebUSB compatible interface.
     * Create a new instance of {@link AdbDaemonWebUsbDeviceManager} using the specified WebUSB implementation.
     * @param usbManager A WebUSB compatible interface.
     */
    public constructor(usb: USB) {
        this._usb = usb;
    public constructor(usbManager: USB) {
        this.#usbManager = usbManager;
    }

    /**
@@ -35,28 +34,20 @@ export class AdbDaemonWebUsbConnectionManager {
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @returns An {@link AdbDaemonWebUsbConnection} instance if the user selected a device,
     * @returns An {@link AdbDaemonWebUsbDevice} 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<AdbDaemonWebUsbConnection | undefined> {
    ): Promise<AdbDaemonWebUsbDevice | undefined> {
        try {
            const device = await this._usb.requestDevice({
            const device = await this.#usbManager.requestDevice({
                filters,
            });
            return new AdbDaemonWebUsbConnection(device, filters, this._usb);
            return new AdbDaemonWebUsbDevice(device, filters, this.#usbManager);
        } catch (e) {
            // No device selected
            // This check is compatible with both Browser implementation
            // and `usb` NPM package from version 2.8.1
            // https://github.com/node-usb/node-usb/issues/573
            if (
                typeof e === "object" &&
                e !== null &&
                "name" in e &&
                e.name === "NotFoundError"
            ) {
            if (isErrorName(e, "NotFoundError")) {
                return undefined;
            }

@@ -74,15 +65,46 @@ export class AdbDaemonWebUsbConnectionManager {
     * but might also have `vendorId`, `productId` or `serialNumber` fields to limit the device list.
     *
     * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
     * @returns An array of {@link AdbDaemonWebUsbConnection} instances for all connected and authenticated devices.
     * @returns An array of {@link AdbDaemonWebUsbDevice} instances for all connected and authenticated devices.
     */
    public async getDevices(
        filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER]
    ): Promise<AdbDaemonWebUsbConnection[]> {
        const devices = await this._usb.getDevices();
        return devices.map(
    ): Promise<AdbDaemonWebUsbDevice[]> {
        const devices = await this.#usbManager.getDevices();
        return devices
            .filter((device) => {
                for (const filter of filters) {
                    if (
                        "vendorId" in filter &&
                        device.vendorId !== filter.vendorId
                    ) {
                        continue;
                    }
                    if (
                        "productId" in filter &&
                        device.productId !== filter.productId
                    ) {
                        continue;
                    }
                    if (
                        "serialNumber" in filter &&
                        device.serialNumber !== filter.serialNumber
                    ) {
                        continue;
                    }

                    try {
                        findUsbAlternateInterface(device, filters);
                        return true;
                    } catch {
                        continue;
                    }
                }
                return false;
            })
            .map(
                (device) =>
                new AdbDaemonWebUsbConnection(device, filters, this._usb)
                    new AdbDaemonWebUsbDevice(device, filters, this.#usbManager)
            );
    }
}
Loading