feat(adb): add state filters to AdbServerClient.prototype.getDevices and AdbServerClient.prototype.trackDevices

This commit is contained in:
Simon Chan 2025-06-16 13:49:16 +08:00
parent 1d3d3c8864
commit a835eb81b5
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
3 changed files with 87 additions and 25 deletions

View file

@ -0,0 +1,5 @@
---
"@yume-chan/adb": minor
---
Add state filters to `AdbServerClient.prototype.getDevices` and `AdbServerClient.prototype.trackDevices`

View file

@ -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<AdbServerClient.Device[]> {
async getDevices(
includeStates: AdbServerClient.ConnectionState[] = [
"device",
"unauthorized",
],
): Promise<AdbServerClient.Device[]> {
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<AdbServerClient.DeviceObserver> {
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;

View file

@ -13,17 +13,28 @@ export function unorderedRemove<T>(array: T[], index: number) {
array.length -= 1;
}
interface Observer {
includeStates: AdbServerClient.ConnectionState[];
onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>;
onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>;
onListChange: EventEmitter<readonly AdbServerClient.Device[]>;
onError: EventEmitter<Error>;
}
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<AdbServerStream> | undefined;
#observers: {
onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>;
onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>;
onListChange: EventEmitter<readonly AdbServerClient.Device[]>;
onError: EventEmitter<Error>;
}[] = [];
#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<AdbServerClient.DeviceObserver> {
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<Error>();
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[];
}
}