diff --git a/.changeset/four-ants-post.md b/.changeset/four-ants-post.md new file mode 100644 index 00000000..4a9707ce --- /dev/null +++ b/.changeset/four-ants-post.md @@ -0,0 +1,5 @@ +--- +"@yume-chan/adb": major +--- + +Sync ADB feature list with latest ADB source code. Some features have been renamed to align with ADB source code. These feature flags are considered implementation details and generally not needed for outside consumers, but it's a breaking change anyway. diff --git a/libraries/adb/src/commands/subprocess/none/service.ts b/libraries/adb/src/commands/subprocess/none/service.ts index 5eb0fd19..09776d8e 100644 --- a/libraries/adb/src/commands/subprocess/none/service.ts +++ b/libraries/adb/src/commands/subprocess/none/service.ts @@ -15,8 +15,14 @@ export class AdbNoneProtocolSubprocessService { } spawn = adbNoneProtocolSpawner(async (command, signal) => { - // `shell,raw:${command}` also triggers raw mode, - // But is not supported on Android version <7. + // Android 7 added `shell,raw:${command}` service which also triggers raw mode, + // but we want to use the most compatible one. + // + // Similar to SSH, we don't escape the `command`, + // because the command will be invoked by `sh -c`, + // it can contain environment variables (`KEY=value command`), + // and shell expansions (`echo "$KEY"` vs `echo '$KEY'`), + // which we can't know how to properly escape. const socket = await this.#adb.createSocket( `exec:${command.join(" ")}`, ); @@ -33,8 +39,10 @@ export class AdbNoneProtocolSubprocessService { command?: string | readonly string[], ): Promise { if (command === undefined) { + // Run the default shell command = ""; } else if (Array.isArray(command)) { + // Don't escape `command`. See `spawn` above for details command = command.join(" "); } diff --git a/libraries/adb/src/commands/subprocess/service.ts b/libraries/adb/src/commands/subprocess/service.ts index 725d11de..235abab5 100644 --- a/libraries/adb/src/commands/subprocess/service.ts +++ b/libraries/adb/src/commands/subprocess/service.ts @@ -25,7 +25,7 @@ export class AdbSubprocessService { this.#noneProtocol = new AdbNoneProtocolSubprocessService(adb); - if (adb.canUseFeature(AdbFeature.ShellV2)) { + if (adb.canUseFeature(AdbFeature.Shell2)) { this.#shellProtocol = new AdbShellProtocolSubprocessService(adb); } } diff --git a/libraries/adb/src/commands/subprocess/shell/service.ts b/libraries/adb/src/commands/subprocess/shell/service.ts index 6ae11305..3b58443b 100644 --- a/libraries/adb/src/commands/subprocess/shell/service.ts +++ b/libraries/adb/src/commands/subprocess/shell/service.ts @@ -12,7 +12,7 @@ export class AdbShellProtocolSubprocessService { } get isSupported() { - return this.#adb.canUseFeature(AdbFeature.ShellV2); + return this.#adb.canUseFeature(AdbFeature.Shell2); } constructor(adb: Adb) { @@ -20,6 +20,7 @@ export class AdbShellProtocolSubprocessService { } spawn = adbShellProtocolSpawner(async (command, signal) => { + // Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details. const socket = await this.#adb.createSocket( `shell,v2,raw:${command.join(" ")}`, ); @@ -45,6 +46,7 @@ export class AdbShellProtocolSubprocessService { service += ":"; if (options) { + // Don't escape `command`. See `AdbNoneProtocolSubprocessService.prototype.spawn` for details. if (typeof options.command === "string") { service += options.command; } else if (Array.isArray(options.command)) { diff --git a/libraries/adb/src/commands/sync/id-common.ts b/libraries/adb/src/commands/sync/id-common.ts new file mode 100644 index 00000000..ba4769ca --- /dev/null +++ b/libraries/adb/src/commands/sync/id-common.ts @@ -0,0 +1,33 @@ +import { getUint32LittleEndian } from "@yume-chan/no-data-view"; + +function encodeAsciiUnchecked(value: string): Uint8Array { + const result = new Uint8Array(value.length); + for (let i = 0; i < value.length; i += 1) { + result[i] = value.charCodeAt(i); + } + return result; +} + +/** + * Encode ID to numbers for faster comparison. + * + * This function skips all checks. The caller must ensure the input is valid. + * + * @param value A 4 ASCII character string. + * @returns A 32-bit integer by encoding the string as little-endian + * + * #__NO_SIDE_EFFECTS__ + */ +export function adbSyncEncodeId(value: string): number { + const buffer = encodeAsciiUnchecked(value); + return getUint32LittleEndian(buffer, 0); +} + +// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd + +export const Lstat = adbSyncEncodeId("STAT"); +export const Stat = adbSyncEncodeId("STA2"); +export const LstatV2 = adbSyncEncodeId("LST2"); + +export const Done = adbSyncEncodeId("DONE"); +export const Data = adbSyncEncodeId("DATA"); diff --git a/libraries/adb/src/commands/sync/id-request.ts b/libraries/adb/src/commands/sync/id-request.ts new file mode 100644 index 00000000..76d31c5b --- /dev/null +++ b/libraries/adb/src/commands/sync/id-request.ts @@ -0,0 +1,12 @@ +import { adbSyncEncodeId } from "./id-common.js"; + +export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js"; + +// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd + +export const List = adbSyncEncodeId("LIST"); +export const ListV2 = adbSyncEncodeId("LIS2"); + +export const Send = adbSyncEncodeId("SEND"); +export const SendV2 = adbSyncEncodeId("SND2"); +export const Receive = adbSyncEncodeId("RECV"); diff --git a/libraries/adb/src/commands/sync/id-response.ts b/libraries/adb/src/commands/sync/id-response.ts new file mode 100644 index 00000000..03f85d91 --- /dev/null +++ b/libraries/adb/src/commands/sync/id-response.ts @@ -0,0 +1,11 @@ +import { adbSyncEncodeId } from "./id-common.js"; + +export { Data, Done, Lstat, LstatV2, Stat } from "./id-common.js"; + +// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/file_sync_protocol.h;l=23;drc=888a54dcbf954fdffacc8283a793290abcc589cd + +export const Entry = adbSyncEncodeId("DENT"); +export const EntryV2 = adbSyncEncodeId("DNT2"); + +export const Ok = adbSyncEncodeId("OKAY"); +export const Fail = adbSyncEncodeId("FAIL"); diff --git a/libraries/adb/src/commands/sync/id.ts b/libraries/adb/src/commands/sync/id.ts new file mode 100644 index 00000000..ced9ddd1 --- /dev/null +++ b/libraries/adb/src/commands/sync/id.ts @@ -0,0 +1,7 @@ +import * as AdbSyncRequestId from "./id-request.js"; +import * as AdbSyncResponseId from "./id-response.js"; + +// Values of `AdbSyncRequestId` and `AdbSyncResponseId` are all generic `number`s +// so there is no point creating types for them + +export { AdbSyncRequestId, AdbSyncResponseId }; diff --git a/libraries/adb/src/commands/sync/index.ts b/libraries/adb/src/commands/sync/index.ts index 2ce53228..565440ec 100644 --- a/libraries/adb/src/commands/sync/index.ts +++ b/libraries/adb/src/commands/sync/index.ts @@ -1,3 +1,4 @@ +export * from "./id.js"; export * from "./list.js"; export * from "./pull.js"; export * from "./push.js"; diff --git a/libraries/adb/src/commands/sync/list.ts b/libraries/adb/src/commands/sync/list.ts index 531cdcb6..e769f4c8 100644 --- a/libraries/adb/src/commands/sync/list.ts +++ b/libraries/adb/src/commands/sync/list.ts @@ -1,8 +1,9 @@ import type { StructValue } from "@yume-chan/struct"; import { extend, string, u32 } from "@yume-chan/struct"; -import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; -import { AdbSyncResponseId, adbSyncReadResponses } from "./response.js"; +import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js"; +import { adbSyncWriteRequest } from "./request.js"; +import { adbSyncReadResponses } from "./response.js"; import type { AdbSyncSocket } from "./socket.js"; import type { AdbSyncStat } from "./stat.js"; import { @@ -36,7 +37,7 @@ export async function* adbSyncOpenDirV2( await adbSyncWriteRequest(locked, AdbSyncRequestId.ListV2, path); for await (const item of adbSyncReadResponses( locked, - AdbSyncResponseId.Entry2, + AdbSyncResponseId.EntryV2, AdbSyncEntry2Response, )) { // `LST2` can return error codes for failed `lstat` calls. diff --git a/libraries/adb/src/commands/sync/pull.ts b/libraries/adb/src/commands/sync/pull.ts index df937288..7dcb0e1e 100644 --- a/libraries/adb/src/commands/sync/pull.ts +++ b/libraries/adb/src/commands/sync/pull.ts @@ -2,8 +2,9 @@ import { ReadableStream } from "@yume-chan/stream-extra"; import type { StructValue } from "@yume-chan/struct"; import { buffer, struct, u32 } from "@yume-chan/struct"; -import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; -import { adbSyncReadResponses, AdbSyncResponseId } from "./response.js"; +import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js"; +import { adbSyncWriteRequest } from "./request.js"; +import { adbSyncReadResponses } from "./response.js"; import type { AdbSyncSocket } from "./socket.js"; export const AdbSyncDataResponse = struct( diff --git a/libraries/adb/src/commands/sync/push.ts b/libraries/adb/src/commands/sync/push.ts index 1b93f5e6..7c53ed0c 100644 --- a/libraries/adb/src/commands/sync/push.ts +++ b/libraries/adb/src/commands/sync/push.ts @@ -8,8 +8,9 @@ import { struct, u32 } from "@yume-chan/struct"; import { NOOP } from "../../utils/index.js"; -import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; -import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; +import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js"; +import { adbSyncWriteRequest } from "./request.js"; +import { adbSyncReadResponse } from "./response.js"; import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js"; import { LinuxFileType } from "./stat.js"; diff --git a/libraries/adb/src/commands/sync/request.ts b/libraries/adb/src/commands/sync/request.ts index 0e39029c..09b714df 100644 --- a/libraries/adb/src/commands/sync/request.ts +++ b/libraries/adb/src/commands/sync/request.ts @@ -1,20 +1,5 @@ import { encodeUtf8, struct, u32 } from "@yume-chan/struct"; -import { adbSyncEncodeId } from "./response.js"; - -export const AdbSyncRequestId = { - List: adbSyncEncodeId("LIST"), - ListV2: adbSyncEncodeId("LIS2"), - Send: adbSyncEncodeId("SEND"), - SendV2: adbSyncEncodeId("SND2"), - Lstat: adbSyncEncodeId("STAT"), - Stat: adbSyncEncodeId("STA2"), - LstatV2: adbSyncEncodeId("LST2"), - Data: adbSyncEncodeId("DATA"), - Done: adbSyncEncodeId("DONE"), - Receive: adbSyncEncodeId("RECV"), -} as const; - export const AdbSyncNumberRequest = struct( { id: u32, arg: u32 }, { littleEndian: true }, @@ -26,13 +11,9 @@ export interface AdbSyncWritable { export async function adbSyncWriteRequest( writable: AdbSyncWritable, - id: number | string, + id: number, value: number | string | Uint8Array, ): Promise { - if (typeof id === "string") { - id = adbSyncEncodeId(id); - } - if (typeof value === "number") { await writable.write( AdbSyncNumberRequest.serialize({ id, arg: value }), diff --git a/libraries/adb/src/commands/sync/response.ts b/libraries/adb/src/commands/sync/response.ts index eba2ccf1..663b8c52 100644 --- a/libraries/adb/src/commands/sync/response.ts +++ b/libraries/adb/src/commands/sync/response.ts @@ -2,39 +2,9 @@ import { getUint32LittleEndian } from "@yume-chan/no-data-view"; import type { AsyncExactReadable, StructDeserializer } from "@yume-chan/struct"; import { decodeUtf8, string, struct, u32 } from "@yume-chan/struct"; -import { unreachable } from "../../utils/no-op.js"; +import { unreachable } from "../../utils/index.js"; -function encodeAsciiUnchecked(value: string): Uint8Array { - const result = new Uint8Array(value.length); - for (let i = 0; i < value.length; i += 1) { - result[i] = value.charCodeAt(i); - } - return result; -} - -/** - * Encode ID to numbers for faster comparison - * @param value A 4-character string - * @returns A 32-bit integer by encoding the string as little-endian - * - * #__NO_SIDE_EFFECTS__ - */ -export function adbSyncEncodeId(value: string): number { - const buffer = encodeAsciiUnchecked(value); - return getUint32LittleEndian(buffer, 0); -} - -export const AdbSyncResponseId = { - Entry: adbSyncEncodeId("DENT"), - Entry2: adbSyncEncodeId("DNT2"), - Lstat: adbSyncEncodeId("STAT"), - Stat: adbSyncEncodeId("STA2"), - Lstat2: adbSyncEncodeId("LST2"), - Done: adbSyncEncodeId("DONE"), - Data: adbSyncEncodeId("DATA"), - Ok: adbSyncEncodeId("OKAY"), - Fail: adbSyncEncodeId("FAIL"), -}; +import { AdbSyncResponseId } from "./id.js"; export class AdbSyncError extends Error {} @@ -50,18 +20,14 @@ export const AdbSyncFailResponse = struct( export async function adbSyncReadResponse( stream: AsyncExactReadable, - id: number | string, + id: number, type: StructDeserializer, ): Promise { - if (typeof id === "string") { - id = adbSyncEncodeId(id); - } - const buffer = await stream.readExactly(4); switch (getUint32LittleEndian(buffer, 0)) { case AdbSyncResponseId.Fail: await AdbSyncFailResponse.deserialize(stream); - throw new Error("Unreachable"); + unreachable(); case id: return await type.deserialize(stream); default: @@ -73,13 +39,9 @@ export async function adbSyncReadResponse( export async function* adbSyncReadResponses( stream: AsyncExactReadable, - id: number | string, + id: number, type: StructDeserializer, ): AsyncGenerator { - if (typeof id === "string") { - id = adbSyncEncodeId(id); - } - while (true) { const buffer = await stream.readExactly(4); switch (getUint32LittleEndian(buffer, 0)) { diff --git a/libraries/adb/src/commands/sync/stat.ts b/libraries/adb/src/commands/sync/stat.ts index 152a7e48..6aa1623b 100644 --- a/libraries/adb/src/commands/sync/stat.ts +++ b/libraries/adb/src/commands/sync/stat.ts @@ -1,8 +1,9 @@ import type { StructValue } from "@yume-chan/struct"; import { struct, u32, u64 } from "@yume-chan/struct"; -import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js"; -import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js"; +import { AdbSyncRequestId, AdbSyncResponseId } from "./id.js"; +import { adbSyncWriteRequest } from "./request.js"; +import { adbSyncReadResponse } from "./response.js"; import type { AdbSyncSocket } from "./socket.js"; // https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36 @@ -131,7 +132,7 @@ export async function adbSyncLstat( await adbSyncWriteRequest(locked, AdbSyncRequestId.LstatV2, path); return await adbSyncReadResponse( locked, - AdbSyncResponseId.Lstat2, + AdbSyncResponseId.LstatV2, AdbSyncStatResponse, ); } else { diff --git a/libraries/adb/src/commands/sync/sync.ts b/libraries/adb/src/commands/sync/sync.ts index ffbd31dd..f7dcabd7 100644 --- a/libraries/adb/src/commands/sync/sync.ts +++ b/libraries/adb/src/commands/sync/sync.ts @@ -21,7 +21,7 @@ import { adbSyncLstat, adbSyncStat } from "./stat.js"; export function dirname(path: string): string { const end = path.lastIndexOf("/"); if (end === -1) { - throw new Error(`Invalid path`); + throw new Error(`Invalid absolute unix path: ${path}`); } if (end === 0) { return "/"; @@ -43,25 +43,25 @@ export class AdbSync { protected _socket: AdbSyncSocket; readonly #supportsStat: boolean; - readonly #supportsListV2: boolean; + readonly #supportsLs2: boolean; readonly #fixedPushMkdir: boolean; - readonly #supportsSendReceiveV2: boolean; + readonly #supportsSendReceive2: boolean; readonly #needPushMkdirWorkaround: boolean; get supportsStat(): boolean { return this.#supportsStat; } - get supportsListV2(): boolean { - return this.#supportsListV2; + get supportsLs2(): boolean { + return this.#supportsLs2; } get fixedPushMkdir(): boolean { return this.#fixedPushMkdir; } - get supportsSendReceiveV2(): boolean { - return this.#supportsSendReceiveV2; + get supportsSendReceive2(): boolean { + return this.#supportsSendReceive2; } get needPushMkdirWorkaround(): boolean { @@ -72,15 +72,13 @@ export class AdbSync { this._adb = adb; this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize); - this.#supportsStat = adb.canUseFeature(AdbFeature.StatV2); - this.#supportsListV2 = adb.canUseFeature(AdbFeature.ListV2); + this.#supportsStat = adb.canUseFeature(AdbFeature.Stat2); + this.#supportsLs2 = adb.canUseFeature(AdbFeature.Ls2); this.#fixedPushMkdir = adb.canUseFeature(AdbFeature.FixedPushMkdir); - this.#supportsSendReceiveV2 = adb.canUseFeature( - AdbFeature.SendReceiveV2, - ); + this.#supportsSendReceive2 = adb.canUseFeature(AdbFeature.SendReceive2); // https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361 this.#needPushMkdirWorkaround = - this._adb.canUseFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir; + this._adb.canUseFeature(AdbFeature.Shell2) && !this.fixedPushMkdir; } /** @@ -120,7 +118,7 @@ export class AdbSync { } opendir(path: string): AsyncGenerator { - return adbSyncOpenDir(this._socket, path, this.supportsListV2); + return adbSyncOpenDir(this._socket, path, this.supportsLs2); } async readdir(path: string) { @@ -157,7 +155,7 @@ export class AdbSync { } await adbSyncPush({ - v2: this.supportsSendReceiveV2, + v2: this.supportsSendReceive2, socket: this._socket, ...options, }); diff --git a/libraries/adb/src/daemon/transport.ts b/libraries/adb/src/daemon/transport.ts index ec0c892d..c660f1d9 100644 --- a/libraries/adb/src/daemon/transport.ts +++ b/libraries/adb/src/daemon/transport.ts @@ -14,7 +14,7 @@ import type { AdbTransport, } from "../adb.js"; import { AdbBanner } from "../banner.js"; -import { AdbFeature } from "../features.js"; +import { AdbDeviceFeatures, AdbFeature } from "../features.js"; import type { AdbAuthenticator, AdbCredentialStore } from "./auth.js"; import { AdbDefaultAuthenticator } from "./auth.js"; @@ -23,30 +23,6 @@ import type { AdbPacketData, AdbPacketInit } from "./packet.js"; import { AdbCommand, calculateChecksum } from "./packet.js"; export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001; -// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252 -// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons). -export const ADB_DAEMON_DEFAULT_FEATURES = /* #__PURE__ */ (() => - [ - AdbFeature.ShellV2, - AdbFeature.Cmd, - AdbFeature.StatV2, - AdbFeature.ListV2, - AdbFeature.FixedPushMkdir, - "apex", - AdbFeature.Abb, - // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed. - // No special handling required. - "fixed_push_symlink_timestamp", - AdbFeature.AbbExec, - "remount_shell", - "track_app", - AdbFeature.SendReceiveV2, - "sendrecv_v2_brotli", - "sendrecv_v2_lz4", - "sendrecv_v2_zstd", - "sendrecv_v2_dry_run_send", - AdbFeature.DelayedAck, - ] as readonly AdbFeature[])(); export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024; export type AdbDaemonConnection = ReadableWritablePair< @@ -154,7 +130,7 @@ export class AdbDaemonTransport implements AdbTransport { static async authenticate({ serial, connection, - features = ADB_DAEMON_DEFAULT_FEATURES, + features = AdbDeviceFeatures, initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE, ...options }: AdbDaemonAuthenticationOptions & @@ -251,11 +227,10 @@ export class AdbDaemonTransport implements AdbTransport { ); } - const actualFeatures = features.slice(); if (initialDelayedAckBytes <= 0) { const index = features.indexOf(AdbFeature.DelayedAck); if (index !== -1) { - actualFeatures.splice(index, 1); + features = features.toSpliced(index, 1); } } @@ -267,9 +242,7 @@ export class AdbDaemonTransport implements AdbTransport { arg1: maxPayloadSize, // The terminating `;` is required in formal definition // But ADB daemon (all versions) can still work without it - payload: encodeUtf8( - `host::features=${actualFeatures.join(",")}`, - ), + payload: encodeUtf8(`host::features=${features.join(",")}`), }); banner = await resolver.promise; @@ -289,7 +262,7 @@ export class AdbDaemonTransport implements AdbTransport { version, maxPayloadSize, banner, - features: actualFeatures, + features, initialDelayedAckBytes, preserveConnection: options.preserveConnection, readTimeLimit: options.readTimeLimit, @@ -336,7 +309,7 @@ export class AdbDaemonTransport implements AdbTransport { connection, version, banner, - features = ADB_DAEMON_DEFAULT_FEATURES, + features = AdbDeviceFeatures, initialDelayedAckBytes, ...options }: AdbDaemonSocketConnectorConstructionOptions) { @@ -359,19 +332,19 @@ export class AdbDaemonTransport implements AdbTransport { initialDelayedAckBytes = 0; } - let calculateChecksum: boolean; - let appendNullToServiceString: boolean; + let shouldCalculateChecksum: boolean; + let shouldAppendNullToServiceString: boolean; if (version >= ADB_DAEMON_VERSION_OMIT_CHECKSUM) { - calculateChecksum = false; - appendNullToServiceString = false; + shouldCalculateChecksum = false; + shouldAppendNullToServiceString = false; } else { - calculateChecksum = true; - appendNullToServiceString = true; + shouldCalculateChecksum = true; + shouldAppendNullToServiceString = true; } this.#dispatcher = new AdbPacketDispatcher(connection, { - calculateChecksum, - appendNullToServiceString, + calculateChecksum: shouldCalculateChecksum, + appendNullToServiceString: shouldAppendNullToServiceString, initialDelayedAckBytes, ...options, }); diff --git a/libraries/adb/src/features-value.ts b/libraries/adb/src/features-value.ts new file mode 100644 index 00000000..3e892423 --- /dev/null +++ b/libraries/adb/src/features-value.ts @@ -0,0 +1,47 @@ +// cspell: ignore Libusb +// cspell: ignore devraw +// cspell: ignore Openscreen +// cspell: ignore devicetracker + +// The order follows +// https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/transport.cpp;l=81;drc=2d3e62c2af54a3e8f8803ea10492e63b8dfe709f + +export const Shell2 = "shell_v2"; +export const Cmd = "cmd"; +export const Stat2 = "stat_v2"; +export const Ls2 = "ls_v2"; +/** + * server only + */ +export const Libusb = "libusb"; +/** + * server only + */ +export const PushSync = "push_sync"; +export const Apex = "apex"; +export const FixedPushMkdir = "fixed_push_mkdir"; +export const Abb = "abb"; +export const FixedPushSymlinkTimestamp = "fixed_push_symlink_timestamp"; +export const AbbExec = "abb_exec"; +export const RemountShell = "remount_shell"; +export const TrackApp = "track_app"; +export const SendReceive2 = "sendrecv_v2"; +export const SendReceive2Brotli = "sendrecv_v2_brotli"; +export const SendReceive2Lz4 = "sendrecv_v2_lz4"; +export const SendReceive2Zstd = "sendrecv_v2_zstd"; +export const SendReceive2DryRunSend = "sendrecv_v2_dry_run_send"; +export const DelayedAck = "delayed_ack"; +/** + * server only + */ +export const OpenscreenMdns = "openscreen_mdns"; +/** + * server only + */ +export const DeviceTrackerProtoFormat = "devicetracker_proto_format"; +export const DevRaw = "devraw"; +export const AppInfo = "app_info"; +/** + * server only + */ +export const ServerStatus = "server_status"; diff --git a/libraries/adb/src/features.ts b/libraries/adb/src/features.ts index 7b7553c0..d3591fec 100644 --- a/libraries/adb/src/features.ts +++ b/libraries/adb/src/features.ts @@ -1,15 +1,32 @@ -// The order follows -// https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/transport.cpp;l=77;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd -export const AdbFeature = { - ShellV2: "shell_v2", - Cmd: "cmd", - StatV2: "stat_v2", - ListV2: "ls_v2", - FixedPushMkdir: "fixed_push_mkdir", - Abb: "abb", - AbbExec: "abb_exec", - SendReceiveV2: "sendrecv_v2", - DelayedAck: "delayed_ack", -} as const; +import * as AdbFeature from "./features-value.js"; -export type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature]; +// biome-ignore lint/suspicious/noRedeclare: TypeScript declaration merging for enum-like object +type AdbFeature = (typeof AdbFeature)[keyof typeof AdbFeature]; + +export { AdbFeature }; + +// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252 +// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons). +export const AdbDeviceFeatures = [ + AdbFeature.Shell2, + AdbFeature.Cmd, + AdbFeature.Stat2, + AdbFeature.Ls2, + AdbFeature.FixedPushMkdir, + AdbFeature.Apex, + AdbFeature.Abb, + // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed. + // No special handling required. + AdbFeature.FixedPushSymlinkTimestamp, + AdbFeature.AbbExec, + AdbFeature.RemountShell, + AdbFeature.TrackApp, + AdbFeature.SendReceive2, + AdbFeature.SendReceive2Brotli, + AdbFeature.SendReceive2Lz4, + AdbFeature.SendReceive2Zstd, + AdbFeature.SendReceive2DryRunSend, + AdbFeature.DevRaw, + AdbFeature.AppInfo, + AdbFeature.DelayedAck, +] as readonly AdbFeature[]; diff --git a/libraries/adb/src/server/transport.ts b/libraries/adb/src/server/transport.ts index 9b87440a..e86ff5b5 100644 --- a/libraries/adb/src/server/transport.ts +++ b/libraries/adb/src/server/transport.ts @@ -6,32 +6,10 @@ import type { AdbTransport, } from "../adb.js"; import type { AdbBanner } from "../banner.js"; -import { AdbFeature } from "../features.js"; +import { AdbDeviceFeatures } from "../features.js"; import type { AdbServerClient } from "./client.js"; -export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() => - [ - AdbFeature.ShellV2, - AdbFeature.Cmd, - AdbFeature.StatV2, - AdbFeature.ListV2, - AdbFeature.FixedPushMkdir, - "apex", - AdbFeature.Abb, - // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed. - // No special handling required. - "fixed_push_symlink_timestamp", - AdbFeature.AbbExec, - "remount_shell", - "track_app", - AdbFeature.SendReceiveV2, - "sendrecv_v2_brotli", - "sendrecv_v2_lz4", - "sendrecv_v2_zstd", - "sendrecv_v2_dry_run_send", - ] as readonly AdbFeature[])(); - export class AdbServerTransport implements AdbTransport { #client: AdbServerClient; @@ -52,9 +30,14 @@ export class AdbServerTransport implements AdbTransport { } get clientFeatures() { - // No need to get host features (features supported by ADB server) - // Because we create all ADB packets ourselves - return ADB_SERVER_DEFAULT_FEATURES; + // This list tells the `Adb` instance how to invoke some commands. + // + // Because all device commands are created by the `Adb` instance, not ADB server, + // we don't need to fetch current server's feature list using `host-features` command. + // + // And because all server commands are created by the `AdbServerClient` instance, not `Adb`, + // we don't need to pass server-only features to `Adb` in this list. + return AdbDeviceFeatures; } // eslint-disable-next-line @typescript-eslint/max-params diff --git a/libraries/android-bin/src/am.ts b/libraries/android-bin/src/am.ts index 980853b2..741c5955 100644 --- a/libraries/android-bin/src/am.ts +++ b/libraries/android-bin/src/am.ts @@ -43,7 +43,7 @@ export class ActivityManager extends AdbServiceBase { options: ActivityManagerStartActivityOptions, ): Promise { // Android 8 added "start-activity" alias to "start" - // Use the most compatible command + // but we want to use the most compatible one. const command = buildCommand( [ActivityManager.ServiceName, "start", "-W"], options,