diff --git a/.changeset/neat-taxes-pick.md b/.changeset/neat-taxes-pick.md new file mode 100644 index 00000000..9eb01bf2 --- /dev/null +++ b/.changeset/neat-taxes-pick.md @@ -0,0 +1,5 @@ +--- +"@yume-chan/adb": minor +--- + +Add state filters to `AdbServerClient.prototype.getDevices` and `AdbServerClient.prototype.trackDevices` diff --git a/libraries/adb/src/server/client.ts b/libraries/adb/src/server/client.ts index 404937d9..bc789312 100644 --- a/libraries/adb/src/server/client.ts +++ b/libraries/adb/src/server/client.ts @@ -37,7 +37,13 @@ export class AdbServerClient { static UnauthorizedError = _UnauthorizedError; static AlreadyConnectedError = _AlreadyConnectedError; - static parseDeviceList(value: string): AdbServerClient.Device[] { + static parseDeviceList( + value: string, + includeStates: AdbServerClient.ConnectionState[] = [ + "device", + "unauthorized", + ], + ): AdbServerClient.Device[] { const devices: AdbServerClient.Device[] = []; for (const line of value.split("\n")) { if (!line) { @@ -46,12 +52,8 @@ export class AdbServerClient { const parts = line.split(" ").filter(Boolean); const serial = parts[0]!; - const state = parts[1]!; - if ( - state !== "unauthorized" && - state !== "offline" && - state !== "device" - ) { + const state = parts[1]! as AdbServerClient.ConnectionState; + if (!includeStates.includes(state)) { continue; } @@ -82,6 +84,7 @@ export class AdbServerClient { devices.push({ serial, state, + authenticating: state === "unauthorized", product, model, device, @@ -197,11 +200,16 @@ export class AdbServerClient { * * Equivalent ADB Command: `adb devices -l` */ - async getDevices(): Promise { + async getDevices( + includeStates: AdbServerClient.ConnectionState[] = [ + "device", + "unauthorized", + ], + ): Promise { const connection = await this.createConnection("host:devices-l"); try { const response = await connection.readString(); - return AdbServerClient.parseDeviceList(response); + return AdbServerClient.parseDeviceList(response, includeStates); } finally { await connection.dispose(); } @@ -211,7 +219,7 @@ export class AdbServerClient { * Monitors device list changes. */ async trackDevices( - options?: AdbServerClient.ServerConnectionOptions, + options?: AdbServerDeviceObserverOwner.Options, ): Promise { return this.#observerOwner.createObserver(options); } @@ -551,6 +559,8 @@ export namespace AdbServerClient { export interface Device { serial: string; state: ConnectionState; + /** @deprecated Use {@link state} instead */ + authenticating: boolean; product?: string | undefined; model?: string | undefined; device?: string | undefined; diff --git a/libraries/adb/src/server/observer.ts b/libraries/adb/src/server/observer.ts index 82188c19..d36a9a73 100644 --- a/libraries/adb/src/server/observer.ts +++ b/libraries/adb/src/server/observer.ts @@ -13,17 +13,28 @@ export function unorderedRemove(array: T[], index: number) { array.length -= 1; } +interface Observer { + includeStates: AdbServerClient.ConnectionState[]; + onDeviceAdd: EventEmitter; + onDeviceRemove: EventEmitter; + onListChange: EventEmitter; + onError: EventEmitter; +} + +function filterDeviceStates( + devices: readonly AdbServerClient.Device[], + states: AdbServerClient.ConnectionState[], +) { + return devices.filter((device) => states.includes(device.state)); +} + export class AdbServerDeviceObserverOwner { current: readonly AdbServerClient.Device[] = []; readonly #client: AdbServerClient; + #stream: Promise | undefined; - #observers: { - onDeviceAdd: EventEmitter; - onDeviceRemove: EventEmitter; - onListChange: EventEmitter; - onError: EventEmitter; - }[] = []; + #observers: Observer[] = []; constructor(client: AdbServerClient) { this.#client = client; @@ -52,17 +63,33 @@ export class AdbServerDeviceObserverOwner { if (added.length) { for (const observer of this.#observers) { - observer.onDeviceAdd.fire(added); + const filtered = filterDeviceStates( + added, + observer.includeStates, + ); + if (filtered.length) { + observer.onDeviceAdd.fire(filtered); + } } } if (removed.length) { for (const observer of this.#observers) { - observer.onDeviceRemove.fire(removed); + const filtered = filterDeviceStates( + added, + observer.includeStates, + ); + if (filtered.length) { + observer.onDeviceRemove.fire(removed); + } } } for (const observer of this.#observers) { - observer.onListChange.fire(this.current); + const filtered = filterDeviceStates( + this.current, + observer.includeStates, + ); + observer.onListChange.fire(filtered); } } @@ -104,10 +131,11 @@ export class AdbServerDeviceObserverOwner { } async createObserver( - options?: AdbServerClient.ServerConnectionOptions, + options?: AdbServerDeviceObserverOwner.Options, ): Promise { options?.signal?.throwIfAborted(); + let current: readonly AdbServerClient.Device[] = []; const onDeviceAdd = new EventEmitter< readonly AdbServerClient.Device[] >(); @@ -119,13 +147,27 @@ export class AdbServerDeviceObserverOwner { >(); const onError = new StickyEventEmitter(); - const observer = { onDeviceAdd, onDeviceRemove, onListChange, onError }; + const includeStates = options?.includeStates ?? [ + "device", + "unauthorized", + ]; + const observer = { + includeStates, + onDeviceAdd, + onDeviceRemove, + onListChange, + onError, + } satisfies Observer; // Register `observer` before `#connect`. // So `#handleObserverStop` knows if there is any observer. this.#observers.push(observer); + // Read the filtered `current` value from `onListChange` event + onListChange.event((value) => (current = value)); + let stream: AdbServerStream; if (!this.#stream) { + // `#connect` will initialize `onListChange` and `current` this.#stream = this.#connect(); try { @@ -136,7 +178,8 @@ export class AdbServerDeviceObserverOwner { } } else { stream = await this.#stream; - onListChange.fire(this.current); + // Initialize `onListChange` and `current` ourselves + onListChange.fire(filterDeviceStates(this.current, includeStates)); } const ref = new Ref(options); @@ -156,17 +199,21 @@ export class AdbServerDeviceObserverOwner { options.signal.addEventListener("abort", () => void stop()); } - // eslint-disable-next-line @typescript-eslint/no-this-alias - const _this = this; return { onDeviceAdd: onDeviceAdd.event, onDeviceRemove: onDeviceRemove.event, onListChange: onListChange.event, onError: onError.event, get current() { - return _this.current; + return current; }, stop, }; } } + +export namespace AdbServerDeviceObserverOwner { + export interface Options extends AdbServerClient.ServerConnectionOptions { + includeStates?: AdbServerClient.ConnectionState[]; + } +}