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

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

feat(adb0daemon-webusb): accept exclusionFilters in getDevices and DeviceObserver

parent ea5002bc
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
---
"@yume-chan/adb-daemon-webusb": patch
---

Accept exclusionFilters in getDevices and DeviceObserver
+18 −27
Original line number Diff line number Diff line
@@ -22,39 +22,31 @@ import {
import type { ExactReadable } from "@yume-chan/struct";
import { EmptyUint8Array } from "@yume-chan/struct";

import type { UsbInterfaceFilter } from "./utils.js";
import {
    findUsbAlternateInterface,
    findUsbEndpoints,
    getSerialNumber,
    isErrorName,
} from "./utils.js";
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
import { findUsbEndpoints, getSerialNumber, isErrorName } from "./utils.js";

/**
 * The default filter for ADB devices, as defined by Google.
 */
export const ADB_DEFAULT_INTERFACE_FILTER = {
export const AdbDefaultInterfaceFilter = {
    classCode: 0xff,
    subclassCode: 0x42,
    protocolCode: 1,
} as const satisfies UsbInterfaceFilter;

export function toAdbDeviceFilters(
export function mergeDefaultAdbInterfaceFilter(
    filters: USBDeviceFilter[] | undefined,
): (USBDeviceFilter & UsbInterfaceFilter)[] {
    if (!filters || filters.length === 0) {
        return [ADB_DEFAULT_INTERFACE_FILTER];
        return [AdbDefaultInterfaceFilter];
    } else {
        return filters.map((filter) => ({
            ...filter,
            classCode:
                filter.classCode ?? ADB_DEFAULT_INTERFACE_FILTER.classCode,
            classCode: filter.classCode ?? AdbDefaultInterfaceFilter.classCode,
            subclassCode:
                filter.subclassCode ??
                ADB_DEFAULT_INTERFACE_FILTER.subclassCode,
                filter.subclassCode ?? AdbDefaultInterfaceFilter.subclassCode,
            protocolCode:
                filter.protocolCode ??
                ADB_DEFAULT_INTERFACE_FILTER.protocolCode,
                filter.protocolCode ?? AdbDefaultInterfaceFilter.protocolCode,
        }));
    }
}
@@ -262,7 +254,7 @@ export class AdbDaemonWebUsbConnection
}

export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
    #filters: UsbInterfaceFilter[];
    #interface: UsbInterfaceIdentifier;
    #usbManager: USB;

    #raw: USBDevice;
@@ -287,22 +279,24 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
     */
    constructor(
        device: USBDevice,
        filters: UsbInterfaceFilter[],
        interface_: UsbInterfaceIdentifier,
        usbManager: USB,
    ) {
        this.#raw = device;
        this.#serial = getSerialNumber(device);
        this.#filters = filters;
        this.#interface = interface_;
        this.#usbManager = usbManager;
    }

    async #claimInterface(): Promise<[USBEndpoint, USBEndpoint]> {
    async #claimInterface(): Promise<{
        inEndpoint: USBEndpoint;
        outEndpoint: USBEndpoint;
    }> {
        if (!this.#raw.opened) {
            await this.#raw.open();
        }

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

        if (
            this.#raw.configuration?.configurationValue !==
@@ -336,17 +330,14 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
            );
        }

        const { inEndpoint, outEndpoint } = findUsbEndpoints(
            alternate.endpoints,
        );
        return [inEndpoint, outEndpoint];
        return findUsbEndpoints(alternate.endpoints);
    }

    /**
     * Open the device and create a new connection to the ADB Daemon.
     */
    async connect(): Promise<AdbDaemonWebUsbConnection> {
        const [inEndpoint, outEndpoint] = await this.#claimInterface();
        const { inEndpoint, outEndpoint } = await this.#claimInterface();
        return new AdbDaemonWebUsbConnection(
            this,
            inEndpoint,
+9 −12
Original line number Diff line number Diff line
@@ -3,10 +3,7 @@
import * as assert from "node:assert";
import { describe, it, mock } from "node:test";

import {
    ADB_DEFAULT_INTERFACE_FILTER,
    AdbDaemonWebUsbDevice,
} from "./device.js";
import { AdbDaemonWebUsbDevice, AdbDefaultInterfaceFilter } from "./device.js";
import { AdbDaemonWebUsbDeviceManager } from "./manager.js";

class MockUsb implements USB {
@@ -69,7 +66,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
            assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
            assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
                {
                    filters: [ADB_DEFAULT_INTERFACE_FILTER],
                    filters: [AdbDefaultInterfaceFilter],
                    exclusionFilters: undefined,
                },
            ]);
@@ -85,7 +82,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
            assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
            assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
                {
                    filters: [ADB_DEFAULT_INTERFACE_FILTER],
                    filters: [AdbDefaultInterfaceFilter],
                    exclusionFilters: undefined,
                },
            ]);
@@ -101,7 +98,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
            assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
            assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
                {
                    filters: [ADB_DEFAULT_INTERFACE_FILTER],
                    filters: [AdbDefaultInterfaceFilter],
                    exclusionFilters: undefined,
                },
            ]);
@@ -117,7 +114,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
            assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
            assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
                {
                    filters: [ADB_DEFAULT_INTERFACE_FILTER],
                    filters: [AdbDefaultInterfaceFilter],
                    exclusionFilters: undefined,
                },
            ]);
@@ -136,7 +133,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
                {
                    filters: [
                        {
                            ...ADB_DEFAULT_INTERFACE_FILTER,
                            ...AdbDefaultInterfaceFilter,
                            ...filter,
                        },
                    ],
@@ -162,7 +159,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
                    filters: [
                        {
                            ...filter,
                            ...ADB_DEFAULT_INTERFACE_FILTER,
                            ...AdbDefaultInterfaceFilter,
                        },
                    ],
                    exclusionFilters: undefined,
@@ -185,11 +182,11 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
                {
                    filters: [
                        {
                            ...ADB_DEFAULT_INTERFACE_FILTER,
                            ...AdbDefaultInterfaceFilter,
                            ...filter1,
                        },
                        {
                            ...ADB_DEFAULT_INTERFACE_FILTER,
                            ...AdbDefaultInterfaceFilter,
                            ...filter2,
                        },
                    ],
+45 −15
Original line number Diff line number Diff line
import { AdbDaemonWebUsbDevice, toAdbDeviceFilters } from "./device.js";
import {
    AdbDaemonWebUsbDevice,
    mergeDefaultAdbInterfaceFilter,
} from "./device.js";
import { AdbDaemonWebUsbDeviceObserver } from "./observer.js";
import { isErrorName, matchesFilters } from "./utils.js";
import { isErrorName, matchFilters } from "./utils.js";

export namespace AdbDaemonWebUsbDeviceManager {
    export interface RequestDeviceOptions {
@@ -37,14 +40,30 @@ export class AdbDaemonWebUsbDeviceManager {
    async requestDevice(
        options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
    ): Promise<AdbDaemonWebUsbDevice | undefined> {
        const filters = toAdbDeviceFilters(options.filters);
        const filters = mergeDefaultAdbInterfaceFilter(options.filters);

        try {
            const device = await this.#usbManager.requestDevice({
                filters,
                exclusionFilters: options.exclusionFilters,
            });
            return new AdbDaemonWebUsbDevice(device, filters, this.#usbManager);

            const interface_ = matchFilters(
                device,
                filters,
                options.exclusionFilters,
            );
            if (!interface_) {
                // `#usbManager` doesn't support `exclusionFilters`,
                // selected device is invalid
                return undefined;
            }

            return new AdbDaemonWebUsbDevice(
                device,
                interface_,
                this.#usbManager,
            );
        } catch (e) {
            // No device selected
            if (isErrorName(e, "NotFoundError")) {
@@ -58,26 +77,37 @@ export class AdbDaemonWebUsbDeviceManager {
    /**
     * Get all connected and requested devices that match the specified filters.
     */
    getDevices(filters?: USBDeviceFilter[]): Promise<AdbDaemonWebUsbDevice[]>;
    async getDevices(
        filters_: USBDeviceFilter[] | undefined,
        options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
    ): Promise<AdbDaemonWebUsbDevice[]> {
        const filters = toAdbDeviceFilters(filters_);
        const filters = mergeDefaultAdbInterfaceFilter(options.filters);

        const devices = await this.#usbManager.getDevices();
        return devices
            .filter((device) => matchesFilters(device, filters))
            .map(
                (device) =>
                    new AdbDaemonWebUsbDevice(
        // filter map
        const result: AdbDaemonWebUsbDevice[] = [];
        for (const device of devices) {
            const interface_ = matchFilters(
                device,
                filters,
                options.exclusionFilters,
            );
            if (interface_) {
                result.push(
                    new AdbDaemonWebUsbDevice(
                        device,
                        interface_,
                        this.#usbManager,
                    ),
                );
            }
        }

        return result;
    }

    trackDevices(filters?: USBDeviceFilter[]): AdbDaemonWebUsbDeviceObserver {
        return new AdbDaemonWebUsbDeviceObserver(this.#usbManager, filters);
    trackDevices(
        options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
    ): AdbDaemonWebUsbDeviceObserver {
        return new AdbDaemonWebUsbDeviceObserver(this.#usbManager, options);
    }
}
+26 −7
Original line number Diff line number Diff line
import type { DeviceObserver } from "@yume-chan/adb";
import { EventEmitter } from "@yume-chan/event";

import { AdbDaemonWebUsbDevice, toAdbDeviceFilters } from "./device.js";
import {
    AdbDaemonWebUsbDevice,
    mergeDefaultAdbInterfaceFilter,
} from "./device.js";
import type { AdbDaemonWebUsbDeviceManager } from "./manager.js";
import type { UsbInterfaceFilter } from "./utils.js";
import { matchesFilters } from "./utils.js";
import { matchFilters } from "./utils.js";

/**
 * A watcher that listens for new WebUSB devices and notifies the callback when
@@ -12,7 +16,8 @@ import { matchesFilters } from "./utils.js";
export class AdbDaemonWebUsbDeviceObserver
    implements DeviceObserver<AdbDaemonWebUsbDevice>
{
    #filters: UsbInterfaceFilter[];
    #filters: (USBDeviceFilter & UsbInterfaceFilter)[];
    #exclusionFilters?: USBDeviceFilter[] | undefined;
    #usbManager: USB;

    #onError = new EventEmitter<Error>();
@@ -29,8 +34,12 @@ export class AdbDaemonWebUsbDeviceObserver

    current: AdbDaemonWebUsbDevice[] = [];

    constructor(usb: USB, filters?: USBDeviceFilter[]) {
        this.#filters = toAdbDeviceFilters(filters);
    constructor(
        usb: USB,
        options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
    ) {
        this.#filters = mergeDefaultAdbInterfaceFilter(options.filters);
        this.#exclusionFilters = options.exclusionFilters;
        this.#usbManager = usb;

        this.#usbManager.addEventListener("connect", this.#handleConnect);
@@ -38,13 +47,18 @@ export class AdbDaemonWebUsbDeviceObserver
    }

    #handleConnect = (e: USBConnectionEvent) => {
        if (!matchesFilters(e.device, this.#filters)) {
        const interface_ = matchFilters(
            e.device,
            this.#filters,
            this.#exclusionFilters,
        );
        if (!interface_) {
            return;
        }

        const device = new AdbDaemonWebUsbDevice(
            e.device,
            this.#filters,
            interface_,
            this.#usbManager,
        );
        this.#onDeviceAdd.fire([device]);
@@ -71,5 +85,10 @@ export class AdbDaemonWebUsbDeviceObserver
            "disconnect",
            this.#handleDisconnect,
        );

        this.#onError.dispose();
        this.#onDeviceAdd.dispose();
        this.#onDeviceRemove.dispose();
        this.#onListChange.dispose();
    }
}
Loading