Skip to content

Commit

Permalink
feat(adb0daemon-webusb): accept exclusionFilters in getDevices and De…
Browse files Browse the repository at this point in the history
…viceObserver
  • Loading branch information
yume-chan committed Nov 30, 2024
1 parent c31f2e6 commit 9e828ff
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-readers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@yume-chan/adb-daemon-webusb": patch
---

Accept exclusionFilters in getDevices and DeviceObserver
45 changes: 18 additions & 27 deletions libraries/adb-daemon-webusb/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
}
}
Expand Down Expand Up @@ -262,7 +254,7 @@ export class AdbDaemonWebUsbConnection
}

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

#raw: USBDevice;
Expand All @@ -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 !==
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 9 additions & 12 deletions libraries/adb-daemon-webusb/src/manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
]);
Expand All @@ -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,
},
]);
Expand All @@ -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,
},
]);
Expand All @@ -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,
},
]);
Expand All @@ -136,7 +133,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
{
filters: [
{
...ADB_DEFAULT_INTERFACE_FILTER,
...AdbDefaultInterfaceFilter,
...filter,
},
],
Expand All @@ -162,7 +159,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
filters: [
{
...filter,
...ADB_DEFAULT_INTERFACE_FILTER,
...AdbDefaultInterfaceFilter,
},
],
exclusionFilters: undefined,
Expand All @@ -185,11 +182,11 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
{
filters: [
{
...ADB_DEFAULT_INTERFACE_FILTER,
...AdbDefaultInterfaceFilter,
...filter1,
},
{
...ADB_DEFAULT_INTERFACE_FILTER,
...AdbDefaultInterfaceFilter,
...filter2,
},
],
Expand Down
60 changes: 45 additions & 15 deletions libraries/adb-daemon-webusb/src/manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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 {
Expand Down Expand Up @@ -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")) {
Expand All @@ -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) =>
// filter map
const result: AdbDaemonWebUsbDevice[] = [];
for (const device of devices) {
const interface_ = matchFilters(
device,
filters,
options.exclusionFilters,
);
if (interface_) {
result.push(
new AdbDaemonWebUsbDevice(
device,
filters,
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);
}
}
33 changes: 26 additions & 7 deletions libraries/adb-daemon-webusb/src/observer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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
Expand All @@ -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>();
Expand All @@ -29,22 +34,31 @@ 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);
this.#usbManager.addEventListener("disconnect", this.#handleDisconnect);
}

#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]);
Expand All @@ -71,5 +85,10 @@ export class AdbDaemonWebUsbDeviceObserver
"disconnect",
this.#handleDisconnect,
);

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

0 comments on commit 9e828ff

Please sign in to comment.