Findings from static analysis of the official Pixel Buds companion app APK
(com.google.android.apps.wearables.maestro.companion, see
SCREENSHOTS_PIXEL_BUDS_APP.md). Every entry references a concrete file/path
under reverse-engineering/apk/v<versionName>-<versionCode>/jadx-output/ or
apktool-output/ (see reverse-engineering/APK_VERSIONS.md for which
version(s) have been pulled/analyzed, and
APK_REVERSE_ENGINEERING_PROCEDURE.md for the pull/decompile/search
procedure — including the current, DECISIONS.md ADR-017 AI-assistance
boundary for this kind of work).
Status: no analysis session has been logged yet — this document currently defines the structure and workflow. Populate the sections below as findings come in, one class/finding at a time, following
PROJECT_RULES.md§1 (FACT / HYPOTHESIS / ASSUMPTION) and §3.
Non-destructive-update convention: this document follows the same
convention CAP-NNN-FINDINGS.md files use (PROJECT_RULES.md §3 rule 9a) —
rewrite a finding in place as understanding changes; don't stack
"Correction"/"Update" addendums under the original text. The history of how
a finding changed belongs in git log/git blame and, for anything
significant, CHANGELOG.md — not in this document's own prose. (This differs
from DECISIONS.md/PROTOCOL.md's convention, where dated Update notes are
kept alongside the original text — see PROJECT_RULES.md §3 rule 9a for the
scope distinction.)
Scope reminder (see PROJECT_RULES.md §8, PROJECT.md non-goals): this
analysis covers software the maintainer has legally installed themselves,
performed solely for interoperability with hardware the maintainer personally
owns. No code from the official app is reused in this project's own codebase —
only the behavior it reveals (protocol structure, UUIDs, opcodes) is
reconstructed. Findings correlated with Bluetooth captures are promoted
directly into PROTOCOL.md, per PROJECT_RULES.md §2.
Status legend (consistent with PROTOCOL.md §0):
- 🟢 FACT — directly observed in the decompiled/disassembled code.
- 🟡 HYPOTHESIS — plausible reading of obfuscated or ambiguous code, not yet cross-checked against a capture.
- ⚪ ASSUMPTION — inferred from naming conventions, structure, or comparison to a similar/known library, not directly confirmed.
- 🔴 OPEN QUESTION — genuinely unresolved: no specific hypothesis or working assumption exists yet, only an identified gap.
Version identity, SHA-256, pull date, source device, and provenance are tracked per-version in
reverse-engineering/APK_VERSIONS.md (the git-tracked index — see APK_REVERSE_ENGINEERING_PROCEDURE.md),
not duplicated here. This table covers the technical fields of whichever version is currently the
main analysis target.
| Field | Value |
|---|---|
| Package name | com.google.android.apps.wearables.maestro.companion |
| Current analysis target version | v1.0.955078536-10253511 (see reverse-engineering/APK_VERSIONS.md) |
| Min/target/compile SDK | minSdk 32, targetSdk 36 (adb shell dumpsys package, 2026-08-30); compileSdk not separately confirmed |
| Obfuscation present? | Yes — R8/ProGuard: almost all app-internal classes are flattened into a single defpackage package with short (2–4 char) obfuscated names (e.g. fxm, gbm, fzd, goq), grouped ~19 unrelated lambda bodies into shared synthetic dispatch classes (e.g. gau, see its entry below). A handful of third-party library classes survive unobfuscated (dev.pigweed.pw_tokenizer.Detokenizer, androidx.*) — one Kotlin function-reference metadata string also survives with its pre-obfuscation signature intact (see fsz entry below), which is how the pw_hdlc/pw_rpc link below was found at all. |
| Native libraries present? | Yes, but not libmaestro.so/libgfps.so — only lib/arm64-v8a/libandroidx.graphics.path.so and lib/arm64-v8a/libpw_tokenizer_jni.so, both present only in split_config.arm64_v8a.apk (absent from base.apk). No file named libmaestro/libgfps exists anywhere across base + both splits, and no System.loadLibrary call in the decompiled sources names one either — see §Native libraries and the Open questions on the fxm/gbm entries below for what this means for AGENTS.md §0/§6's native-binary framing. |
| Firmware/library versions referenced in-app | (not yet checked this pass) |
See APK_REVERSE_ENGINEERING_PROCEDURE.md for the full step-by-step procedure
(pulling/storing a new version, the diff-against-previous-version pass, the
keyword-search efficiency techniques, and the exclusion list for
out-of-scope areas). Summary:
apktool d base.apk -o apktool-output/— for resources, manifest, smali.jadx -d jadx-output/ base.apk— for readable (decompiled) Kotlin/Java.- Search
jadx-output/for keywords:BluetoothSocket,BluetoothGatt,BluetoothGattCallback,BluetoothGattCharacteristic(this app uses RFCOMM as the primary control transport perARCHITECTURE.md§1 — don't assume GATT-only)UUID.fromStringMessageStream,AccountKey,FastPair(Fast Pair SDK usage, relevant toPROTOCOL.md§2.1/§4.3)BluetoothHidDevice,HidDeviceProfile,HidDeviceAppSdpSettings(added 2026-08-23 — the project has a live, evidenced Bluetooth HID hypothesis: HID-Control/HID-Interrupt L2CAP channels observed during SDP inCAP-002, and anAndroidHeadTrackerHID Feature report decoded inCAP-016; seeARCHITECTURE.md§1/§15. Significant enough that missing it on the first APK pass would be a real gap, not a cosmetic one — check for it explicitly rather than relying on the genericble/bluetoothkeywords below to surface it.)- Package/class names containing
ble,bluetooth,buds,wearable,headset,maestro,gfps,hid .proto-generated classes (look forGeneratedMessageLite,builder(), field names matching known UI strings likeanc_mode,eq_band)
- For
.protoschema extraction specifically, usepbtk(AGENTS.md§4/§6) rather than hand-reconstructing schemas from decompiled getter/setter names alone — protobuf field numbers are not always recoverable from JADX output. - Document every relevant class below, even if its name is obfuscated — record the obfuscated name plus a readable alias you assign for reference.
- Cross-check any candidate opcode, UUID, or message code against a real
capture before promoting it from 🟡/⚪ to 🟢 in
PROTOCOL.md— static analysis alone does not confirm runtime behavior.
Template per class — copy for each new finding. Every finding must cite the exact decompiled file and line number (e.g.
reverse-engineering/apk/v1.2.3-45/jadx-output/sources/com/google/.../Xy2.java:142), not only a class name — this is whatPROJECT_RULES.md§1 rule 3 already requires of aREVERSE_ENGINEERING.mdevidence source; the Path field below must always include:line_number, never a bare file path.
All entries below are from v1.0.955078536-10253511 (reverse-engineering/APK_VERSIONS.md),
surfaced by an AI mechanical §4 keyword-search pass (DECISIONS.md ADR-017) and written up here at
the maintainer's explicit request — the FACT/HYPOTHESIS/ASSUMPTION labels below follow
PROJECT_RULES.md §1's rule that static analysis alone is 🟢 FACT only for "this code exists and
does X," never for a protocol-behavior claim; every protocol-relevance reading stays 🟡/⚪ with a
Hypothesis test until cross-checked against a capture. None of this has been promoted to
PROTOCOL.md, and no DECISIONS.md ADR has been written or superseded — both remain a maintainer
call per AGENTS.md §6/§15.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fzd.java:9 - Readable alias: InternalRfcommUuidRegistry
- Role: 🟢 FACT: interface holding two 128-bit UUID constants used elsewhere (see
gbmbelow) to select between two internal RFCOMM sockets. Each UUID appears in both a canonical byte order and a byte-reversed form — 4UUID.fromStringliterals total; confirmed by manually reversing the byte sequence of each pair, they are the same 2 UUIDs, not 4. Static fieldb(fzd.java:12-18) is a map normalizing any of the 4 string forms to one of the 2 canonical UUIDs. - Relevant UUIDs found:
3a046f6d-24d2-7655-6534-0d7ecb759709/099775cb-7e0d-3465-5576-d2246d6f043a(same UUID, byte-reversed pair) and25e97ff7-24ce-4c4c-8951-f764a708f7b5/b5f708a7-64f7-5189-4c4c-ce24f77fe925(same UUID, byte-reversed pair) — cross-reference §UUID register. - Relevant message groups/codes found: none (this is RFCOMM/SDP-layer, not Fast Pair Message Stream).
- Relevant methods:
- static initializer
fzd.java:9-18— builds the UUID list (a) and the normalization map (b).
- static initializer
- Hypothesis test: not applicable to this entry alone (constants only) — see
gbm's entry. - Open questions: which of these two UUIDs (if either) matches the SDP record UUID actually
observed on the wire for DLCI 0x02 (Pigweed
pw_hdlc) vs. DLCI 0x08 (still-unidentified private envelope) inPROTOCOL.md§2.2a/§2.3's table — not yet checked against a capture.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gbm.java:35 - Readable alias: InternalRfcommSocketSelector
- Role: 🟢 FACT:
a()(gbm.java:20-47) selects between two RFCOMM socket implementations based on which UUID(s) are present in a discovered UUID set, logging"Provide pigweed internal rfcomm socket"(gbm.java:36, if either "pigweed" UUID form fromfzdmatches),"Provide default internal rfcomm socket"(gbm.java:39, if either "default" UUID form matches), or falling back to the pigweed implementation otherwise (gbm.java:42-43). 🟡 HYPOTHESIS, now capture-corroborated (see Hypothesis test below — still 🟡, promotion to 🟢 inPROTOCOL.mdis a maintainer call perAGENTS.md§6, not made here): the "pigweed"-labeled UUID (25e97ff7-...) is the RFCOMM service UUID for the Pigweedpw_hdlc-encoded channel AGENTS.md §6 already identifies as DLCI 0x02. - Relevant UUIDs found: the same 4 forms (2 canonical UUIDs) as
fzd, imported viafzd's registry. - Relevant methods:
a():20-47— the selection logic described above.
- Hypothesis test — result (checked 2026-08-30, three independent capture sessions): for each of
CAP-001,CAP-002,CAP-032(bluetooth.addr == 04:00:6e:cf:6e:07), the SDP Service Search Attribute Response lists25e97ff7-24ce-4c4c-8951-f764a708f7b5among the discovered service UUIDs, and the matching Protocol Descriptor List response resolves it to RFCOMM server channel 1 in every session (btsdp.data_element.value.uuid_128/btsdp.protocol.channel:CAP-001frame 1327 @ 42.534s,CAP-002frame 1327 @ 42.534s — same elapsed offset, likely the same setup ritual, not a bug —CAP-032frame 1632 @ 104.943s). Channel 1 is then opened withSent SABM Channel=1/Rcvd UA Channel=1, andtshark's ownbtrfcomm.dlcifield reads0x02for every frame on that channel, in all three captures (CAP-001frame 1334 @ 42.545s,CAP-032frame 1645 @ 105.173s) — i.e. RFCOMM server channel 1 is DLCI 0x02 on the wire, not just by the2×channelarithmetic. This is a direct, three-session-reproducible SDP-to-DLCI correlation: the "pigweed" UUID this pass found ingbm.java/fzd.javais the actual SDP-advertised service UUID for the channel AGENTS.md §6 calls DLCI 0x02. Maintainer sign-off obtained 2026-08-30 (DECISIONS.mdADR-018, Option 2 — narrow promotion): channel ownership (this is the companion app's own internal RFCOMM socket) is now 🟢 FACT inPROTOCOL.md§2.2a/§2.3; that the Sent-direction payload content specifically carrieslibmaestro's settings-write commands remains 🟡 HYPOTHESIS (strong), unchanged — seePROTOCOL.md§2.2a for the exact scope of what was and wasn't promoted. - Open questions: what the "default internal rfcomm socket" (
3a046f6d-...) corresponds to — DLCI 0x08's still-unidentified private envelope, or something else entirely. Exhaustive negative result (checked 2026-08-30): a raw-byte scan (both byte-order forms) of every capture file in this project's possession — all 23*btsnoop_hci.logfiles, the 3 additional*btsnooz_hci.logfiles (CAP-012,CAP-013,CAP-031— missed by the first pass, since that used a*btsnoop_hci.log-only glob;fileconfirms all three are genuine "BTSnoop version 1, HCI UART (H4)" despite the "btsnooz" filename, not a compressed/different format), and the two nRF Connect logs (CAP-014-nrf-connection.log,CAP-017-nRF.txt) — found zero occurrences of either UUID form anywhere. The two nRF logs were confirmed to be BLE-only GATT connection logs (gatt = device.connectGatt(..., TRANSPORT_LE, ...)), which cannot structurally contain a Bluetooth Classic SDP/RFCOMM service UUID at all — a non-result by construction, not a gap. Also checked: everyCAP-NNN-FINDINGS.md/CAP-NNN-EVENT-NOTES.mdfor a prior manual mention (none found). Conclusion: the "default internal rfcomm socket" has not been observed on the wire in any capture taken so far, in either byte order, across every capture format this project has collected — genuinely unexplained, not merely unsearched. Plausible next step: a fresh capture specifically covering whatever app action/condition triggers the app to pick the "default" branch ingbm.a()instead of "pigweed" (not yet identified what that condition is —gbm.javaalone doesn't show what determines which UUID ends up in the discovered SDP set). - Update (2026-08-30, follow-up pass) — the "default" branch's own construction traced; found to
build a plain
gbd, notfut; that plaingbd's base read/write methods are a structural match to DLCI 0x08's still-unidentified envelope shape. This was found while tracinggbb/gbc/the UUID connection wiring behindnpv(see thenqx/npy/nqo/npw/nqmentry's own Update above) — included here, not there, since it's squarely this entry's own open question.- 🟢 FACT (code existence/structure):
gbm's constructor (gbm(npo npoVar, npo npoVar2, npo npoVar3), fieldsa/b/c) is called from exactly one site,defpackage/fqg.java:465:new gbm(this.u, c8, this.x)— i.e.this.b("default",gbm.java:39) = the localc8, andthis.c("pigweed",gbm.java:36) =this.x. Tracingc8's own definition (fqg.java:463):npo c8 = nph.c(new fwy(this.r, this.v, ..., 3, ...));— i.e. the "default" provider is built fromfwycase arm3(defpackage/fwy.java:44-46), a different factory class fromfuu's case 0 (which buildsfut, the "pigweed" branch, per the already-documented chain:frg.javacases 7–12 →fut.f→ HDLC).fwycase 3's body:return new gbd(((gbc) this.c).a(), ((gbn) this.e).a(), fux.b(), (ExecutorService) npoVar2.a(), (gos) this.a.a(), (ftk) this.b.a());— a barenew gbd(...), notnew fut(...). Sincefutis confirmed the only class extendinggbdanywhere in this APK (per the prior pass'sgrep -rl "extends gbd" .), this plaingbdinstance runs with no overridden behavior — it usesgbd's own base-classc()(read loop) andd(int, int, byte[])(write) methods directly, notfut's HDLC-specific overrides. Notably, this "default" branch'sgbdis constructed with the samegbninstance as an argument (((gbn) this.e).a(),fwy.java:46) — sincegbnhas only one construction site total in the app (defpackage/fqg.java:623), this is very likely the identical singleton object also feedingfut's construction, i.e. both the "default" and "pigweed" branches are wired through the same UUID-discovery/fzd-registry chain — the two branches differ in which wire-protocol wrapper object (futvs. plaingbd) is attached, not in an independent UUID-matching mechanism each. (This last point aboutgbninstance identity was not independently confirmed against the surroundingfqg.javaconstructor's full field-reuse pattern — a giant, heavily R8-merged generated-DI class where field slots are visibly reused across unrelated construction phases, so it is stated here as a strong but not fully certain reading, not confirmed FACT.) - The structural match, found on reading
gbd's own base methods (already partly transcribed in this doc's owngbdentry above, re-examined here for this specific comparison):gbd.c()(read loop) doesint readUnsignedByte = this.r.readUnsignedByte(); byte readByte = this.r.readByte(); short readShort = this.r.readShort(); ... mxr t = mxr.t(bArr, 0, readShort);— i.e. 1-byte + 1-byte + 2-byte-length + value.gbd.d(int i, int i2, byte[] bArr)(write) mirrors it exactly:this.q.writeByte(i); this.q.writeByte(i2); this.q.writeShort(length); this.q.write(bArr);. Both use plainjava.io.DataInputStream/DataOutputStream(gbd.java:4-5, confirmed stock JDK classes, no custom byte-order override), whosereadShort/writeShortare big-endian by Java Language Specification. This is a structural, field-for-field match toPROTOCOL.md§2.1/§2.3's already-documented DLCI 0x08 envelope shape,[Group:1][Code:1][Length:2B-BE][Value](CAP-001-FINDINGS.md§2,CAP-004-FINDINGS.md§5a). - 🟡 HYPOTHESIS (strong, code-level structural match only — not capture-cross-checked with a real
UUID observed on DLCI 0x08's own wire traffic):
gbm's "default internal rfcomm socket" branch (UUID3a046f6d-24d2-7655-6534-0d7ecb759709/099775cb-..., via the plaingbdbuilt infwy.javacase 3) is a strong candidate for the code-level owner ofPROTOCOL.md§2.3's still-unidentified DLCI 0x08 channel — the "third, still-unidentified private[Group][Code][Length][Value]envelope" that document's own table already flags as structurally similar to §2.1's Fast Pair shape but with its own Group/Code numbering. This is consistent with, and offers one plausible explanation for, this same entry's own already-recorded "exhaustive negative result" above (the3a046f6d-.../099775cb-...UUID has never been observed in any capture's SDP records) — if this reading is correct, that non-observation would simply mean none of this project's captures happened to include the exact moment the app performs SDP discovery while this "default" service is being advertised/queried, not that the UUID/socket doesn't exist. This is not itself capture evidence and does not, by itself, resolve §2.3's open question — it is a code-level structural lead pointing at which app-side code path to look for on the far (transport) side if/when a capture does show DLCI 0x08's SDP UUID; perAGENTS.md§6, an AI agent does not promote this inPROTOCOL.md— it is recorded here as a candidate for the maintainer. - Hypothesis test: a fresh capture that (a) captures the full SDP Service Search Attribute
Response/Protocol Descriptor List exchange (not just post-connection RFCOMM traffic, which is
what most existing captures focus on), and (b) opens DLCI 0x08, would let two things be checked
directly: whether
3a046f6d-24d2-7655-6534-0d7ecb759709(or its byte-reversed form) appears in the SDP response and resolves to the same RFCOMM channel number DLCI 0x08 is observed on (mirroring exactly the methodology this entry's own Hypothesis test already used to confirm the "pigweed" UUID → DLCI 0x02, above) — a positive match would be strong, direct wire-level confirmation of this pass's code-level reading. - Update (2026-08-30, Tier 0 re-decode task) — a narrower, cheaper hypothesis test than the SDP-UUID
one above run and confirmed: the
[Length:2B]field's byte order is empirically big-endian, not just "consistent withgbd'sDataInputStream/DataOutputStreamusage" as this entry stated structurally. 🟢 FACT (empirical, re-extracted from the raw logs, not merely re-read from a prior finding): every non-zero-length DLCI 0x08 frame inCAP-001-btsnoop_hci.log(9 frames,tshark -r CAP-001-btsnoop_hci.log -Y "btrfcomm.dlci==0x08 and btrfcomm.len>0" -T fields -e frame.number -e data.data) andCAP-004-btsnoop_hci.log(5 complete, non-fragmented frames, same filter) was checked by comparing the 2-byte Length field interpreted as big-endian vs. little-endian against the frame's own actual trailing byte count. 14/14 matched big-endian exactly; 0/14 matched little-endian (e.g.CAP-001frame 1089,05 0a 00 0d <13 bytes>: BE reading of00 0d= 13 = the actual value length; LE reading = 3328, nowhere close). This is a direct, cross-capture (2 independent sessions) empirical confirmation of the byte-order half of this entry's own structural claim (gbd.c()/d()usingDataInputStream.readShort()/DataOutputStream.writeShort(), which are big-endian per the Java Language Specification) — it does not by itself confirm the SDP-UUID attribution (the Hypothesis test immediately above, which remains untested), only the framing-mechanism-level match already asserted. SeeCAP-004-FINDINGS.md's 2026-08-30 addendum for the full per-frame table and reproduction script (added there rather than here, since that file is this project's existing authority for DLCI 0x08's envelope decode).
- 🟢 FACT (code existence/structure):
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gau.java:39 - Readable alias: RfcommUuidNormalizer
- Role: 🟢 FACT:
gauis an R8-merged synthetic lambda-dispatch class (implements orz, constructed asnew gau(<int>)from ~20 unrelated call sites) — onlycase 3is protocol-relevant; the other arms handle unrelated features (OOBE state, device SKU lookup, hearing-wellness notifications, screen-on/off listening, etc.) and are out of scope here.case 3(gau.java:39):UUID.fromString((String) fzd.b.get(((UUID) obj).toString()))— normalizes an input UUID (potentially the byte-reversed form) to its canonical form viafzd's registry. - Relevant methods:
a(Object):39(one arm of the sharedswitchina(Object),gau.java:22-156).
- Open questions: none beyond
fzd/gbm's.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gbd.java:10 - Readable alias: InternalRfcommConnection
- Role: 🟢 FACT: wraps a
BluetoothSocketwith aDataOutputStream q/DataInputStream rpair (gbd.java:20-21) for whichever "internal" RFCOMM channelgbmselected.gbd.java:211closes the underlyingBluetoothSocket. This is the actual read/write connection object for whichever of the twofzdUUIDs was matched. - Relevant methods:
- constructor
gbd.java:26-35. - socket close path around
gbd.java:211.
- constructor
- Open questions: full read/write/frame-decoding logic not traced this pass — this pass only
located the class, not its
pw_hdlcframe handling in detail.
defpackage.fua / defpackage.gax / defpackage.gbo / defpackage.gba / defpackage.hjy — Group-numbered SparseArray router on gbd (DLCI 0x02), and its per-device-variant handler registration
Added 2026-08-30, maintainer-approved write-up of a candidate cluster first surfaced during an independent verification of a now-deleted, unofficial
REVIEW_REPORT.md. That document's central technical claim — that this router says something about DLCI 0x04 (the official Fast Pair Message Stream) being GMS-exclusive — is a channel conflation and is explicitly not repeated here:fuasends viagbd(InternalRfcommConnection, entry above), whichDECISIONS.mdADR-018 already confirms wraps DLCI 0x02 (the companion app's own "pigweed"/Maestro internal socket), not DLCI 0x04 at all. Nothing below concerns DLCI 0x04.Second occurrence (2026-08-30, same day): an independently-submitted cross-validation report (Antigravity/Gemini 3.1 Pro), reviewed the same day as the write-up above, reproduced this identical conflation over the identical file set, plus an additional, likewise-unsupported "battery handler" framing for
gba/gaa— see the dated update at the end of this entry.
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fua.java(class declaration; methodsl()/m()/o()) -
Readable alias: MaestroGroupRouter
-
Role: 🟢 FACT (code exists and does this): implements
gbk, ftm. Holds aSparseArraykeyed by integer Group ID (this.m.put(i, fzyVar)to register a handler,(fzy) this.m.get(i)to dispatch an inbound message) and sends outbound messages viagbdVar.d(i3, i2, bArr)— i.e. throughgbd's ownd(int, int, byte[])write method ([Group:1][Code:1][Length:2][Value], seegbd's entry above), on the DLCI 0x02 connectiongbmalready selected. 🟡 HYPOTHESIS: this is the concreteftm-interface router implementation thegbm/fzdcluster's Group-numbered dispatch was inferred to need but hadn't been located until this pass. -
Relevant message groups/codes found: registers whichever Group IDs its callers pass in (see
gax/gbobelow) — not itself a fixed list. -
Hypothesis test: not yet run against a capture — no DLCI 0x02 payload has been decoded to the point of reading its own leading Group byte against this router's registered set (§2.2a's own "Sent"-direction content remains opaque).
-
Open questions: how an inbound DLCI 0x02 payload's Group byte is actually extracted before reaching
this.m.get(i)— not traced this pass. -
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gax.java:20-26 -
Readable alias: PrestoGroupHandlerRegistration
-
Role: 🟢 FACT: logs
"Registering handlers for Presto device: %s"and callsftmVar.h(135, this.c); ftmVar.h(136, this.d); ftmVar.h(130, this.e);— registering handlers for Group130/135/136(decimal) against afua-shaped router, for a device variant internally labeled "Presto." -
Open questions: 🔴 whether "Presto" refers to the Pixel Buds Pro 2 specifically is unconfirmed, not settled — this project's own memory/
CHANGELOG.md2026-08-30 entry already flags a different internal codename, "presto" (viaqjn/gaa.java), as very likely an alternate product's settings schema, not the Buds Pro 2's own. Whethergax's "Presto" is the same product-labeling scheme asqjn's is not checked this pass. Groups 130/135/136 are not yet cross-checked against any capture. -
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gbo.java:15-20 -
Readable alias: StrettoGroupHandlerRegistration
-
Role: 🟢 FACT: same pattern as
gax, logging"Registering handlers for Stretto device"and registering Groups135/136/5for a device variant labeled "Stretto." Not cross-checked against any capture. -
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gba.java(class declaration,implements fzy; methodb(int, mxr)) -
Readable alias: GroupHandlerCandidate130
-
Role: ⚪ ASSUMPTION only — JADX reports
b(int, mxr)'s method body as "Method not decompiled." PerAPK_REVERSE_ENGINEERING_PROCEDURE.md§6's JADX-misdecompile note, this needs anapktoolsmali-output fallback read before any content claim; not attempted this pass. -
Open questions: full handler body content — blocked on the smali fallback above.
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/hjy.java:1-40 -
Readable alias: ScheduledTimeoutHandler (tentative)
-
Role: 🟢 FACT: implements
fzy; owns its ownScheduledExecutorService/ScheduledFuturewith a 10000ms schedule, in a context that looks OTA-timeout-related. 🔴 Explicitly not established: that this is the only scheduler used by any battery-related handler —ScheduledExecutorServiceappears in roughly 78 files project-wide, so a claim like "the onlyScheduledExecutorServiceused by the battery-Group handlers ishjy, for OTA" (as the now-deletedREVIEW_REPORT.mdasserted) is not supported by this alone and should not be repeated without a dedicated scope read. -
Open questions: what Group(s), if any, actually route to this class via
fua's router; full relevance read not done this pass. -
Update (2026-08-30, second cross-validation pass, independent full-tree re-check on
gba/gaa/hjy): the same external report referenced in this entry's header note namedgba.java(Group130) andgaa.java(Group135) as "the battery handlers" and concluded, from those two files plushjy.java, that no battery-polling loop exists in this cluster. Re-checked this pass, independently:- The "battery handler" premise itself is unsupported.
gba.java's only method body remains undecompiled (see its own entry above — no claim about its content is possible either way).gaa.javahas zerobattery/Batterystring references anywhere in its 1068 lines (grep -n "battery\|Battery" gaa.java→ no hits), and its own independently-documented role (see theqhx/qjn/qjtentry below) is a settings-response notification dispatcher (OOBE mode, the "presto"-labeled possibly-different-product settings groups) — nothing ties it to battery telemetry. - Full-tree grep (
jadx-output/sources/, not a sample) for the two Android battery intent/action strings the external report cited:ACTION_BATTERY_LEVEL_CHANGED→ 0 matches project-wide;ACTION_VENDOR_SPECIFIC_HEADSET_EVENT→ 0 matches project-wide. This specific narrow claim is confirmed true, independently of the unsupported "battery handler" framing around it. hjy.java's scope reconfirmed as OTA-only, not general battery telemetry: itsScheduledExecutorService-driven timeout (hjy.java:30, 10000ms) and its logic (hjy.java:33- 65) reference"Attempt manual OTA",OTA_ERROR_BATTERY_LOW,OTA_ERROR_NOT_DOCKED,OTA_SUCCESS— "battery low" appears only as one OTA precondition failure reason, not as a battery-telemetry channel.- This project's own already-🟢-FACT battery-push mechanism (
PROTOCOL.md§4.3 Option E, DLCI 0x08's private envelope,Group 0x0e Code 0x01/Group 0x04 Code 0x03) is a different channel entirely from this DLCI-0x02fua/gbdcluster — so even where the report's high-level claim ("battery updates are event-driven, not app-polled") happens to be directionally consistent with what this project has independently confirmed by capture, it is not actually supported by the APK evidence the report presents.
- The "battery handler" premise itself is unsupported.
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fxm.java:10 -
Readable alias: MaestroSoftwareInfoAndHidUuidCheck
-
Role: 🟢 FACT, two behaviors in one class: (a) holds the official Bluetooth SIG HID Profile UUID
00001124-0000-1000-8000-00805f9b34fb(fxm.java:12) and, ini()(fxm.java:78-117), checks whether a connected device's discovered UUID set already contains it — if not, logs"Could not find HID UUID, fetchUuidsWithSdp."(fxm.java:107) and callsBluetoothDevice.fetchUuidsWithSdp()(fxm.java:110) to trigger a fresh SDP UUID fetch. (b) Inc()/i()(fxm.java:46-91), issues fourmaestro_pw.Maestro/GetSoftwareInfopw_rpc unary calls, one per combination of{MAESTRO_A, MAESTRO_B} x {LEFT_BT_CORE, RIGHT_BT_CORE}(seegoqentry below). -
Relevant UUIDs found:
00001124-0000-1000-8000-00805f9b34fb(official Bluetooth SIG HID Profile UUID). -
Relevant message groups/codes found: n/a (this is pw_rpc, not Fast Pair Message Stream).
-
Relevant methods:
c(goq,goq):46-65— builds oneGetSoftwareInfopw_rpc unary call for a given source/target route pair.i():78-117— firesGetSoftwareInfofor all 4 route pairs, then runs the HID-UUID-presence check described above.
-
Hypothesis test: correlate against
CAP-002/CAP-016's existing HID captures — does an SDP fetch (triggered by this code path) precede the HID-Control/HID-Interrupt L2CAP channel setup already observed there? -
Open questions: what
GetSoftwareInfo's actual pw_rpc request/response payload looks like on the wire — not yet captured or decoded.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fsz.java:75 - Readable alias: MaestroWriteSettingRpcClient
- Role: 🟢 FACT: issues a
maestro_pw.Maestro/WriteSettingpw_rpc call (fsz.java:75). A Kotlin function-reference metadata string atfsz.java:223—"getWriteSettingMethodClient(Lcom/google/android/apps/wearables/maestro/companion/pw/hdlc/RouteProto$Route;)Ldev/pigweed/pw_rpc/MethodClient;"— literally names the app's owncom.google.android.apps.wearables.maestro.companion.pw.hdlc.RouteProto$Routeclass and the upstreamdev.pigweed.pw_rpc.MethodClientclass. This string escaped R8 renaming, unlike almost everything else indefpackage, because Kotlin method-reference metadata must retain original signatures for reflection — it is the single strongest static-analysis confirmation this pass found that the app's own vocabulary for this transport is literally Pigweedpw_rpcoverpw_hdlc. 🟡 HYPOTHESIS:WriteSettingis likely the generic pw_rpc call used for settings this project cares about (ANC, EQ, etc.) not already covered by the Fast Pair Message Stream's Group0x08opcode (PROTOCOL.md§4.1) — not yet confirmed against a capture or a decoded request payload. - Relevant methods:
- the method whose reference is captured at
fsz.java:223(getWriteSettingMethodClient; the call site itself is obfuscated and not further traced this pass).
- the method whose reference is captured at
- Hypothesis test: capture a setting change made through DLCI 0x02 (the Pigweed channel candidate
from
gbm's entry) and check whether its payload, oncepw_hdlc-decoded, resolves to aWriteSettingpw_rpc unary call for a setting not already explained by the Message Stream Group 0x08 opcode. - Open questions: full list of settings routed through
WriteSettingvs. the Message Stream path.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fut.java:220 - Readable alias: HdlcSendLogger
- Role: 🟢 FACT: logs
"Fail to send hdlc encoded message"on a send failure — corroboratesfsz.java:223'spw_hdlcpackage reference using the app's own log vocabulary, independently of that one surviving metadata string. - Open questions: which higher-level class calls into this failure path, and under what conditions — not traced this pass.
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/fux.java:9-120(a()is a singleswitch (this.a)dispatch method, same R8-merged-lambda pattern asgau; eachcasebuilds onenqsservice-descriptor object) -
Readable alias: MaestroPwRpcServiceCatalog
-
Role: 🟢 FACT: this pass re-read
fux.javain full (superseding the earlier partial read, which covered only 5 of the 6maestro_pw.*services and, critically, missedmaestro_pw.Maestroitself —case 8,fux.java:55-66— the serviceWriteSetting/GetSoftwareInfo/ReadSettingactually belong to). Eachnqs.c(name, requestDefaultInstance, responseDefaultInstance)call names the exact request/response message class for that RPC method (via each class's ownX.asingleton default-instance field, e.g.qjc.a) — this is a strictly richer reading than method names alone, now decodable withscripts/decode_rawmessageinfo.py(see the new class entries below for what each type actually contains). Concrete service/method/type table, all still 🟢 FACT-for-code-existence only (protocol meaning stays 🟡/⚪ per entry):Service Method Kind Request type Response type fux.javalinemaestro_pw.DosimeterFetchDailySummariesunary nia(empty)qhz:27maestro_pw.DosimeterSubscribeToLiveDbserver-stream nia(empty)qir:27maestro_pw.EartipFitTestStartTestunary qii(empty)nia(empty):41maestro_pw.EartipFitTestSubscribeToResultsserver-stream qih(empty)qig:41maestro_pw.EartipFitTestEndTestunary qif(empty)nia(empty):41maestro_pw.JitterBufferSetJitterBufferSizePreferenceunary qjsnia(empty):50maestro_pw.MaestroGetSoftwareInfounary nia(empty)qjb:57maestro_pw.MaestroGetHardwareInfounary nia(empty)qiv:58maestro_pw.MaestroSubscribeRuntimeInfoserver-stream nia(empty)qiy:59maestro_pw.MaestroSetWallclockunary qiznia(empty):60maestro_pw.MaestroWriteSettingunary qjcnia(empty):61maestro_pw.MaestroReadSettingunary qixqja:64maestro_pw.MaestroSubscribeToSettingsChangesserver-stream nia(empty)qja:64maestro_pw.MaestroSubscribeToOobeActionsserver-stream nia(empty)qje:64maestro_pw.MultipointSubscribeToQuietModeStatusserver-stream nia(empty)qjp:69maestro_pw.MultipointForceMultipointSwitchunary qijnia(empty):69maestro_pw.HeadGestureStartDetectionunary qip(empty)nia(empty):112maestro_pw.HeadGestureSubscribeToResultsserver-stream qio(empty)qin:112maestro_pw.HeadGestureEndDetectionunary qim(empty)nia(empty):112Also present in
fux.javabut not pursued this pass (out of this session's WriteSetting/EQ/ANC focus, not out of project scope — plain infra/OTA services, not settings-relevant):case 1/12buildpw.software_update-style OTA method catalogs (SetManualOta,StartRightBudUpdate,GetRunningVersion, etc.,fux.java:31-37/82-88);case 3is the generic pw_rpcEchoService(:44);case 6isa10a20.kpi.Kpi/KpiStream(:52);case 7ispw.log.Logs/Listen(:54);case 10ispw.software_update.BundledUpdate(:74);case 13/14buildgoq-keyed integer maps unrelated to any RPC service (:89-104);case 4/11/18/19return unrelated pre-built objects (gok/nse/gam/gay, not traced).⚪ ASSUMPTION (unchanged from before): service names map fairly literally to app features (Dosimeter = sound-exposure/hearing-health dosimeter; EartipFitTest = the
eartipsealfit-test UI already found underui/settings/sound/eartipseal/; HeadGesture = the head-gesture feature AGENTS.md §6 already references) — inferred from naming, not confirmed against UI flow or a capture. -
Hypothesis test: for each service, capture the corresponding UI action (e.g. running an ear-tip fit test, triggering a head gesture, changing ANC/EQ) per
TESTPLAN_BLUETOOTH_HCI_SNOOP.md, and check whether the resulting DLCI 0x02 traffic decodes (viapw_hdlc) to a call against that service/method name, then whether the payload's field layout matches the corresponding type's decoded structure below. -
Open questions: whether all
maestro_pw.*services travel over the same RFCOMM channel (the "pigweed" UUID fromgbm) or are split across more than one.
Tooling note — scripts/decode_rawmessageinfo.py (protobuf-lite RawMessageInfo compact-schema-string decoder)
This APK's protobuf-lite runtime uses GeneratedMessageLite.newMessageInfo(default, infoString, objects) (the compact schema-string codegen style), not the older per-field
CodedInputStream.readXxx() calls pbtk's jar_extract.py regex-parses — pbtk cannot parse this
codegen style at all (confirmed root cause, not a scope/targeting issue; see
WORKSTATION_PREPARATIONS.md's pbtk section). scripts/decode_rawmessageinfo.py is a small,
dependency-free Python decoder for this format, written this pass. It is a direct, field-for-field
port of the real parsing algorithm — not a guess or approximation, per PROJECT_RULES.md §1's
"operate with zero creativity" rule — copied from three files fetched from the upstream
protocolbuffers/protobuf GitHub repo (main branch, fetched 2026-08-30):
java/core/src/main/java/com/google/protobuf/RawMessageInfo.java (info-string header/flags
decoding), .../MessageSchema.java's newSchemaForRawMessageInfo method (the full field-entry
parsing loop, byte offsets, and the Object[] array consumption order), and
.../FieldType.java (the field-type id table, values 0–50; oneof types are 51 + <base type id>
per MessageSchema.ONEOF_TYPE_OFFSET). Validated against this project's own two already-documented
trivial marker types (nia, qib, both \u0004\u0000/\u0001\u0000 → 0 real fields, matching
their known-trivial role) before being trusted on anything else. Usage:
python3 scripts/decode_rawmessageinfo.py <path/to/Class.java> [more files...] — finds every
new naa(<default>, "<info-string>", <objectsArrayOrNull>) construction in each file (the pattern
REVERSE_ENGINEERING.md's naa/myp mapping above already confirmed byte-for-byte matches Google's
own public RawMessageInfo class) and prints each message's decoded field list: field number, wire
type, oneof/map-ness, and — where the Object[] array entry is a Java string literal — the raw
(usually short, obfuscated: "b", "c", ...) declared field name. This is a purely mechanical,
code-structure decode (🟢 FACT-for-code-existence per field, exactly as strong as reading the same
structure by hand from the smali/JADX output would be) — it does not itself establish wire-protocol
meaning; every semantic reading below is separately labeled 🟡/⚪ and requires its own capture
correlation, per AGENTS.md §6/§15 and PROJECT_RULES.md §1.
defpackage.qjc / defpackage.qja — MaestroSettingOneof (WriteSetting request body / ReadSetting+SubscribeToSettingsChanges response body)
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/qjc.java:28and.../qja.java:28 -
Readable alias: MaestroSettingOneof
-
Role: 🟢 FACT (decoded via
scripts/decode_rawmessageinfo.py, cross-checked by hand against the raw info-string before trusting the script's output):qjcis the exactWriteSettingrequest type named infux.java:61(nqs.c("WriteSetting", (mzw) qjc.a.a(7, null), ...)), andqjais the exactReadSettingresponse /SubscribeToSettingsChangesresponse type (fux.java:64). Both decode to byte-for-byte the same 5-field, single-oneof shape: aoneof(Java fieldsb=case selectorint,c=valueObject, matching each class's own field declarations, e.g.qjc.java:8-9:public int b = 0; public Object c;) with 5 alternatives, oneMESSAGE-typed field per proto field number 1–5:- field 1 →
qhx(1 field — see below) - field 2 →
qjn(12 fields — see below) - field 3 →
qjt(10 fields, sparse field numbers 1–14 — see below) - field 4 →
qhr(38 fields — see below, the headline finding of this pass) - field 5 →
qjv(3 fields — see below)
This is a genuine "one
Settingvalue, one of 5 top-level categories, each category itself containing a further oneof of individual settings" shape — used identically as theWriteSettingrequest payload and as theReadSetting/SubscribeToSettingsChangesresponse payload, which is itself evidence (🟡 HYPOTHESIS, code-structure-only) that these three RPC methods share one generic "the current/desired value of setting X" representation, consistent with a request/response-mirror pattern rather than 3 independently-shaped payloads. - field 1 →
-
Cross-reference to
PROTOCOL.md§4.5 /DECISIONS.mdADR-013: ADR-013 promoted to 🟢 FACT (from wire evidence alone, independent of this static-analysis pass) that DLCI 0x02's general-purpose settings-write envelope has the outer shapefield 5 { field 4 { ... } }. This pass's independent, code-derived finding is structurally consistent with that:qjc's field-number 4 alternative isqhr— i.e. the wire-observed "field 4" is plausiblyqjc'sqhralternative, andqhrin turn contains its own 38-way inner oneof (the "..." ADR-013 did not further decompose). This is not a new promotion — ADR-013's own scope note already stated only the outer wrapper shape was promoted, not any individual field's meaning — but it is new, independently-derived static evidence for the same structural claim, from a completely different evidence path (APK code vs. wire bytes), which strengthens without changing either's status. (ADR-013 describes only a 2-levelfield5{field4{...}}nesting from the wire; this pass's 3-levelqjc.field4→qhr.fieldNreading is a finer decomposition of what ADR-013's own "..." covered, not a contradiction of it — see Open questions below for the one thing that doesn't fully reconcile.) -
Open questions: ADR-013 describes the outer wrapper as
field5{field4{...}}— i.e. field 5 is the outermost tag observed on the wire. This pass's reading hasqjcitself (containing field-4qhr) as one level inside whatever carries it — plausibly thepayloadfield (field number 5) of Pigweed's ownpw_rpc.RpcPacketwire envelope (the standardpw_rpcpacket proto haspayloadat field 5), which would make the full nestingRpcPacket.field5(payload) → qjc.field4(qhr) → qhr.fieldN— three levels, of which ADR-013 (working from raw wire bytes, not from this session's just-recoveredpw_rpc.RpcPacket/qjcschemas) apparently only distinguished two. This reconciliation is a plausible reading, not confirmed — nopw_rpc.RpcPacket-shaped class has been located/decoded this pass. A capture correlation (redecoding an existing DLCI 0x02 "Sent" frame's raw hex against this 3-level structure) would be needed to confirm or refute it. -
Hypothesis test: pick any existing capture with a DLCI-0x02 Sent-direction frame already identified as
field5{field4{...}}(CAP-019–CAP-024per ADR-013), and check whether the "..." bytes further decompose asqhr's own oneof tag structure (\x08-prefixed varint field-number, wire-type 0/2/5 depending on field 9/10/13'sANC_STATEenum vs. field 16/18'sqjwmessage vs. a plain bool) — this would be a direct, byte-level confirmation or refutation of the reconciliation above. -
Update (2026-08-30, follow-up pass) — the
pw_rpc.RpcPacket-shaped class this reconciliation needed has now been located and decoded; the 3-level nesting reading is structurally confirmed at the code level (still not capture-correlated). 🟢 FACT (code existence/structure, viascripts/decode_rawmessageinfo.py):defpackage/nqx.javadecodes to exactly 7 fields, numbers 1–7, matching the standard upstream Pigweedpw_rpc/internal/packet.protoRpcPacketmessage field-for-field:Proto field nqx's type (decoder output)Java field Standard pw_rpc.RpcPacketfield1 ENUM btype2 UINT32 cchannel_id3 FIXED32 dservice_id4 FIXED32 emethod_id5 BYTES fpayload6 UINT32 gstatus7 UINT32 hcall_idThis is not just a shape match — the class that actually populates
nqxwas also traced and its field assignments match this table exactly:defpackage/npy.java, methoda(int, nqm, BiFunction, mzq)(npy.java:34-80), builds annqxand setsnqxVar.b = a.at(2)(field 1/type, from a channel-ID-adjacent int — see caveat below),.c = npwVar.a(field 2/channel_id, taken directly fromdefpackage/npw.java's ownafield —npw's constructor rejectsa <= 0with the message"The channel ID must be positive", i.e.npwis itself Pigweed'sChannelclass),.d/.efromnqpVar.a().b/nqpVar.b.a()(fields 3/4,service_id/method_id— both ultimately derived frommne.E(<fully-qualified name string>), a string-hash function, exactly matching upstreampw_rpc's own name-hashing scheme for service/method IDs, not sequential integers), and — the critical field for this reconciliation —.f = mzqVar.d()(npy.java:67-73, only whenmzqVar != null) wheremzqVaris the outgoing protobuf request message itself (qjcforWriteSetting) and.d()serializes it to bytes. This directly confirms field 5 (payload,BYTES) is populated with the serializedqjcmessage — i.e. the full nesting is confirmed at the code level exactly as this entry's Open Questions originally speculated:RpcPacket.field5(payload, BYTES) → qjc(deserialized).field4(qhr) → qhr.fieldN. The chainfyv.a(qjc)(fyv.java:44-49) →esk'snqoVar.e(qjcVar)(already documented) →nqo.e(mzq)(nqo.java:64-67,c(nql.UNARY); return (nqe) b(new nqn(), mzqVar);) →nqo.b(...)(nqo.java:34-40) →((npy) this.b).a(...)was traced end-to-end this pass and is consistent throughout —nqoispw_rpc.MethodClient(its own constructornqo(npy, int, nqm, nqu)atnqo.java:94-99takes exactly aClient, a channel id, aMethoddescriptor, and call options, matching that class's real API shape) andnpyispw_rpc.Client.- One caveat, not fully resolved: field 1/
type's value comes froma.at(2)(defpackage/a.java:842-844:i == 1 ? myy.b() : i - 2), a generic, widely-reused protobuf-lite enum ordinal-to-wire-value helper (the same three-line body appears asa.as/a.at/etc. for many unrelated enum types across this codebase) — for input2this evaluates to0. Whether wire value0is the correctPacketType.REQUEST(or similar) enum number for this specific call, or whether this generic helper is being applied to the wrong enum type by this reading, was not independently verified this pass (would require locating the actualPacketType-equivalent enum class and confirming its declared values) — this one field's exact value is left open; the field-position/field-5-payload finding above does not depend on it. - What this confirms vs. does not: confirms, at the code level, that
qjc/qhr's 3-level nesting reading is structurally real (not merely plausible) — the app's own RPC-send code genuinely wraps a serializedqjcinsideRpcPacket.payload(field 5). It does not confirm this against real wire bytes from a capture — theHypothesis testabove (redecoding a DLCI 0x02 Sent frame's raw hex against this 3-level structure) is unchanged and still the needed next step for a capture-level confirmation; this update only removes the "nopw_rpc.RpcPacket-shaped class has been located" caveat that previously blocked even attempting that redecode.
- One caveat, not fully resolved: field 1/
-
Hypothesis test — result (2026-08-30, Tier 0 re-decode task, byte-level, capture-correlated): 🟢 FACT (mechanical byte decode against the confirmed structures above, independently re-extracted from the raw
.logfiles, not merely re-read from a priorCAP-NNN-FINDINGS.md): two existing DLCI 0x02 Sent frames already identified asfield5{field4{...}}inCAP-020-FINDINGS.md§3/§4 (CAP-020,TOUCH-001/HEAD-001) were re-pulled directly fromCAP-020-btsnoop_hci.logviatshark -r CAP-020-btsnoop_hci.log -Y "btrfcomm.dlci==0x02 and frame.number==1741" -T fields -e data.data(frame 1741:7e004b0310151dea71de7d5e251d9a8c9e2a0422022001c5a08a3c7e; frame 1935:7e004b0310151dea71de7d5e251d9a8c9e2a052203e801020641623b7e— byte-identical to the file's own citation), HDLC-unescaped/CRC-verified perPROTOCOL.md§2.2a, and the inner "..." bytes (after the 13-byte prefix,field5,field4) decomposed one level further as standard protobuf wire-format tags. Frame 1741 (TOUCH-001): inner bytes20 01decode to tag0x20= field 4, wiretype 0 (varint), value1— an exact match, at the tag-byte level, toqhr's own field 4 (BOOL, "Head/touch gestures master enable toggle" per this document's ownqhrfield-register table, write sitefyo.java:124-144). Frame 1935 (HEAD-001): inner bytese8 01 02decode to a 2-byte LEB128 tag0xe8 0x01=232= field 29, wiretype 0 (varint), value2— matchingPROTOCOL.md§4.5.4's existing wire-derived HYPOTHESIS ("field 29 = Head gestures") and confirming, for the first time at the byte level, that this document's ownqhrfield-register table's "field 29 | ENUM | not found [write site]" entry was a gap in the mechanical call-site search, not a sign the field is unwritten — a realSentframe does populate it, with a plain 1-byte varint value (2) consistent with a small-ordinalENUM. This is a direct confirmation of the reconciliation this entry's Open Questions section proposed: the wire's "..." decomposes exactly asqhr's own oneof tag structure, field-number-for-field-number, not merely "plausible." Reproducible directly from the hex above plusPROTOCOL.md§2.2a's published unescape/CRC method and standard protobuf wire-format tag decoding (field = tag>>3,wiretype = tag&7) — no APK-derived script needed. SeeCAP-020-FINDINGS.md's 2026-08-30 addendum for the full capture-side write-up, including the reproduction script. Promoted toPROTOCOL.md§2.2a as 🟢 FACT, 2026-08-30, maintainer sign-off (DECISIONS.mdADR-019) — scoped exactly as stated above (2 sampled fields, not full-schema coverage).
defpackage.nqx / defpackage.npy / defpackage.nqo / defpackage.npw / defpackage.nqm — Pigweed pw_rpc wire-packet & client plumbing (nqx = RpcPacket)
- Path:
nqx.java:1-52,npy.java:1-133,nqo.java:1-114,npw.java:1-67,nqm.java:1-53, allreverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/ - Readable alias:
nqx=RpcPacket,npy=Client,nqo=MethodClient,npw=Channel,nqm=Method (descriptor) - Role: 🟢 FACT (code existence/structure,
nqx's shape viascripts/decode_rawmessageinfo.py; the rest via direct reading and call-graph tracing) — found this pass while pursuing Goal 2 of a 2026-08-30 follow-up task (locate thepw_rpcpacket envelope class).dev.pigweed.pw_rpcdoes not survive as an actual package/class path in this APK (find . -path '*/dev/pigweed/*'only turns updev/pigweed/pw_tokenizer/Detokenizer.java— real, unobfuscated); the only prior evidence ofpw_rpc.MethodClient's existence was a surviving Kotlin function-reference metadata string infsz.java:223("getWriteSettingMethodClient(...)Ldev/pigweed/pw_rpc/MethodClient;"), naming the return type of a lambda but not the obfuscated class itself. This pass traced the actual call chain from that lambda's real target (fsz.javacase 2,fsz.java:65-76:npy.f(int, "maestro_pw.Maestro", "WriteSetting")→nqo) down through the real send path and identified all 5 classes:nqx=pw_rpc.RpcPacket— see theqjc/qjaentry above (Update, 2026-08-30) for the full field-by-field decode and the field-5/payloadconfirmation; not repeated here.npy=pw_rpc.Client: holds the channel/service table (npy.java:20-24) and exposesf(int channelId, String service, String method)(:130-132) →e(int, int, int)(:116-128, hashes the service/method name strings viamne.E(...)and looks up/creates annqo) anda(int, nqm, BiFunction, mzq)(:34-80, the method that actually builds and populates thenqxRpcPacket, per theqjc/qjaentry's Update above, then hands the serialized bytes tonpwVar.a(...)— theChannel's own send method).nqo=pw_rpc.MethodClient: its 4-argument constructornqo(npy, int, nqm, nqu)(nqo.java:94-99) takes exactly(Client, channelId, Method, defaultCallOptions), matching the realMethodClientAPI shape.nqo.e(mzq)(:64-67) is the confirmed unary-RPC-invoke method (c(nql.UNARY)guards against invoking a non-unary method this way, matchingpw_rpc's own "invoked as the wrong RPC type" runtime check,nqo.java:42-48's exact error message wording is itself a close paraphrase of upstream Pigweed's own client-side error text) — this is the methodesk.java's already-documentednqoVar.e(qjcVar)call site (case 19) actually calls.npw=pw_rpc.Channel:npw(int i, npv npvVar)(npw.java:14-20) throwsIllegalArgumentExceptionwith the message"The channel ID must be positive: %d is invalid"ifi <= 0— a direct, distinctive match to Pigweed's ownChannelconstructor validation. Fielda= channel ID (feedsnqx/RpcPacketfield 2), fieldb= the channel's output interface (npv, not traced further this pass — presumably Pigweed'sChannelOutput, the interface a transport like this app's RFCOMM/HDLC socket write would implement).nqm=pw_rpc.Method(descriptor): holds a service reference (a, typenqs— presumablypw_rpc'sServicedescriptor, not independently traced this pass), a method-name string (b), and annql(method-kind enum —nql.UNARY/nql.SERVER_STREAMINGare both referenced fromnqo.java, matchingpw_rpc's ownMethodTypeenum). Itsa()method (nqm.java:20-22) returnsmne.E(this.b)— a string-hash of the method name — confirmingservice_id/method_idinnqxare Pigweed's standard 32-bit name-hash IDs, not sequential integers, exactly matching upstreampw_rpc's actual ID-assignment scheme (pw_rpc::internal::Hash()).
- What this does NOT establish:
npv/nqs/nql/nqu/nqp/mne.E's own internals were referenced but not independently opened and read this pass (out of scope for Goal 2, which only asked to locate and decode the packet envelope itself); the exact numeric value Pigweed'stypefield takes for aWriteSettingrequest (a.at(2)→0, see theqjc/qjaentry's caveat above) was not independently verified against a namedPacketType-equivalent enum. - Open questions: what
npv(Channel output interface) is actually implemented by for this app's RFCOMM/HDLC transport — tracing that would be the natural next step to close the loop from "app code builds anRpcPacket" to "bytes actually observed on DLCI 0x02", but was not attempted this pass. - Update (2026-08-30, follow-up pass) —
npv's implementation traced end-to-end; the loop from "app code builds anRpcPacket" to "bytes on the wire" is now closed, and independently corroboratesPROTOCOL.md§2.2a's wire-derived HDLC framing byte-for-byte, from the opposite (encode-side) evidence direction. 🟢 FACT (code existence/structure and call-chain tracing; the framing-mechanism match itself is a direct structural comparison, not a semantic claim):defpackage/npv.javais confirmed a single-method interface:void a(byte[] bArr), matchingnpw.a(byte[])'s delegation (npw.java:23-25).- All 6 real
npvimplementations (grep -rn "new npv" .finds none taggedimplements npvas a named class — all 6 are anonymous inner classes, JADX-merged under the labelfui) live indefpackage/frg.java,a()methodcases 7–12 (frg.java:47-238). Each constructs annpw(Channel) for one(goq route target) × (goq.MAESTRO_A | goq.MAESTRO_B core)pair —CASE,LEFT_BT_CORE,RIGHT_BT_COREunderMAESTRO_A(cases 7–9) and the same three underMAESTRO_B(cases 10–12) — i.e. 6 distinct pw_rpcChannels, one per (route target, core) combination, all usinggoq(already-documentedMaestroRouteTargetenum). Every one of the 6 anonymousnpv.a(byte[])bodies is byte-identical in structure: aswitchon a captured constantint(0–5, one per case) that calls((fut) b.a()).f(bArr, goq.<TARGET>)— i.e. all 6 channels funnel through the same class,fut, differing only in whichgoqtarget constant they pass. defpackage/fut.java(class fut extends gbd implements fwv) is confirmed as the only class in this APK extendinggbd(grep -rl "extends gbd" .→fut.javaalone) — i.e.futis not one of several sibling RFCOMM-connection implementations, it is the concrete implementation of the already-documentedgbd/InternalRfcommConnectionabstract base for this app version. Itsd(int, int, byte[])override (fut.java,@Override // defpackage.gbd) unconditionally throwsnew IOException("Unsupported legacy communication style.")— i.e.gbditself supports (or once supported) more than one wire style, butfutonly implements the HDLC-based one.fut.f(byte[] bArr, goq goqVar)is a byte-for-byte code-level match toPROTOCOL.md§2.2a's already-wire-confirmed HDLC framing, evidenced line-by-line:- Writes literal byte
126(0x7E) — the opening HDLC flag, matching §2.2a exactly. - Builds
j = ((a2 & 15) << 6) | ((a3 & 15) << 10)— combining the local channel's owngoqaddress (a2, read fromthis.x, the sameoql/Set<goq>built in the constructor fromnmxVar2.a(), i.e. the other end of the 6-channel table) and the target'sgoqaddress (a3 = goqVar.a()) — then LEB128-varint-encodesj(thewhile (j2 >>>= 7) { i2++; }counting loop, then thewhile (j3 = j >>> 7; ...) { allocate.put(...) }encode loop with the standard "continuation bit on all but the last byte" pattern) — a direct implementation of §2.2a's already-confirmed "HDLC Address field, LEB128-varint-encoded (1–3+ bytes)". - Writes a single Control byte, value
3(0x03) — the standard HDLC "UI" (Unnumbered Information) frame type, matching §2.2a's "single Control byte" and thepbpctrl-cited "wrapped in ... U-frames" description. - Writes the payload bytes (the
RpcPacket-serializedbArrargument) viagvx.q(wrap, crc32, byteArrayOutputStream). - Computes and writes a 4-byte little-endian CRC-32 (
bArr3[0]=(byte)crc32.getValue(); bArr3[1..3]=...>>8/>>16/>>24) over the unescaped Address+Control+Data — an exact match to §2.2a's "CRC-32 (IEEE 802.3/zlib polynomial, little-endian byte order)". - Writes the closing flag byte
126(0x7E).
- The escape/byte-stuffing step (
defpackage/gvx.java:332-344, static methodq(ByteBuffer, CRC32, OutputStream), called from all 3 write steps above) checks each byte: if it equals126(0x7E) it writesgop.a; if125(0x7D) it writesgop.b; otherwise the raw byte — accumulating every byte (pre-escape) into the running CRC.defpackage/gop.java:6-7definesa = {125, 94}({0x7D, 0x5E}) andb = {125, 93}({0x7D, 0x5D}) — i.e.0x7Eis escaped as0x7D 0x5Eand0x7Das0x7D 0x5D, the exact byte values §2.2a's own unescape formula (Xtransmitted as0x7D (X XOR 0x20)) predicts (0x7E XOR 0x20 = 0x5E,0x7D XOR 0x20 = 0x5D) — confirmed, not just consistent. - The final socket write:
fut.fwrites the fully-framedByteArrayOutputStreamtothis.u.j()(fut.java, inheritedufield fromgbd, typeffd).defpackage/ffd.java:374-375'sj()method returns((BluetoothSocket) this.a).getOutputStream()— the literal AndroidBluetoothSocket's own output stream, i.e. this is the actual, final RFCOMM socket write, with no further abstraction layers between it and the OS Bluetooth stack.
- Writes literal byte
- Full confirmed send chain, code-traced end-to-end this pass: UI/gesture trigger →
fye.a(qhs)or equivalent (buildsqjc) →fyv.c/fyv.a(qjc)→esk→nqo.e(qjc)(MethodClient.invoke) →npy.a(...)(Client, builds & serializes thenqx/RpcPacket,payload=serializedqjc) →npw.a(bytes)(Channel) →npv.a(bytes)(one offrg.java's 6 anonymous implementations) →fut.f(bytes, goq)(HDLC-encode: flag + LEB128 address + control + payload + CRC-32 + flag, with0x7D-escaping) →ffd.j()=BluetoothSocket.getOutputStream(). - What this does NOT establish: this is a code-level confirmation that the app's encoder
implements the same algorithm §2.2a already confirmed from decoding real captured bytes — it is
strong, independent corroboration from the opposite evidence direction, comparable in kind to how
this project already treats two-independent-path agreement elsewhere, but it does not itself
constitute a new capture and changes nothing about §2.2a's already-🟢-FACT status (this update
does not need to promote anything — the target was already FACT). It also does not independently
confirm which of the 6
(route target × core)channels corresponds to the DLCI 0x02 socket already tied to SDP UUID25e97ff7-24ce-4c4c-8951-f764a708f7b5in ADR-018 —futbeing the solegbdsubclass strongly suggests all 6 channels multiplex over that one socket (one RFCOMM connection, many logicalpw_rpcchannels distinguished by the Address field'sgoq-derived value), consistent with §2.2a's own note that "two distinct address values are observed... not a fixed/exhaustive set" andDESKRESEARCH_FINDINGS.md§6's0x1e80/0x2680/0xe980additional values — but this reading (one socket, N logical channels multiplexed via the Address field) was not independently checked againstgbb/gbc/fuu.java's own socket-selection wiring this pass (fuu.java:37-60, the factory that constructsfut, was opened but not traced further).
- Open questions (remaining): whether
gbb/gbc(the objectsfuu.java's factory passes intofut's constructor) tie specifically to the one already-documented Pigweed-internal-socket UUID (ADR-018), or whether a different/secondgbd-based construction path exists for some other socket — not traced this pass; whatnpv's address-combination formula'sa2/local-address side represents concretely (whichgoqvalue the app itself uses as its own end of the channel, i.e. is it alwaysgoq.HOSTor does it vary) was read but not independently interpreted. - Update (2026-08-30, follow-up pass) —
gbb/gbcconfirmed NOT UUID-carrying; the actual UUID-aware connection step traced to a sharedgbnsingleton; a second, previously-unexaminedgbd-construction path found along the way, with a significant unplanned finding for DLCI 0x08 (see the extendedgbmentry below for the full write-up — not repeated here in full to respect this doc's non-destructive-update/non-duplication convention). 🟢 FACT (code existence/structure):defpackage/gbb.javais a plain wrapper aroundOptional<BluetoothDevice>(a()returns the device's MAC address,b()its reflection-basedisConnected()state) — it carries no UUID or socket-selection information at all, ruling out the original guess thatgbb/gbcthemselves might encode which socket/UUID is in play. The actual UUID-driven step is one constructor argument further along:fut's (and, newly found this pass, the basegbd's own)oql/this.aconstructor argument comes fromgbn.a()(defpackage/gbn.java:14-19), a class with exactly one construction site in the whole decompiled tree (defpackage/fqg.java:623, i.e. a singleton in this app's dependency graph), whosea()method builds its connection-observable by filtering throughfzd.a— the same UUID list already documented in thegbm/fzdentries below — viagau's already-documentedcase 3normalizer. See thegbmentry below for what this pass found about the other branch's construction (fwy.javacase 3) and its structural match to DLCI 0x08's envelope shape.
defpackage.qhr — MaestroSettingGroup4 (38-field oneof; contains 3 fields of the qhs ANC_STATE_* enum)
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/qhr.java:29 -
Readable alias: MaestroSettingGroup4 (field-4 alternative of
qjc/qja's top-level oneof) -
Role: 🟢 FACT (code existence/structure): a single oneof with 38
MESSAGE/BOOL/ENUM/SINT32alternatives, proto field numbers 1–38, all sharing the sameb(case)/c(value) field pair. Full decode (python3 scripts/decode_rawmessageinfo.py qhr.java): fields 1–6, 8, 11, 14, 15, 19–22, 25, 27, 28, 30, 32–34, 36, 38 = plainBOOL; field 17 =SINT32; fields 7, 12, 16, 18, 23, 31, 35, 37 =MESSAGE(nested typesqju,qht,qjw×2,qhq,qiq,qjf,qis— see their own entries below); fields 9, 10, 13 =ENUMof typeqhs; fields 24, 26, 29 =ENUMof types referenced only asqgx.n/qgx.s/qgx.r(opaque validity-checker instances delegating to an obfuscated shared helpera.aO(int)— not independently named, unlikeqhs, so no further reading attempted this pass). Headline finding: the enum class backing fields 9, 10, and 13 — reached viaqgx.j→case 9: return qhs.b(i2) != null;(qgx.java:56) — isdefpackage.qhs(qhs.java:5-10), and its constants are unobfuscated, human-readable literals:ANC_STATE_UNKNOWN(0),ANC_STATE_OFF(1),ANC_STATE_ACTIVE(2),ANC_STATE_AWARE(3),ANC_STATE_ADAPTIVE(4). This is a direct, code-level (not naming-convention-inferred) match in kind toPROTOCOL.md§4.1's already-🟢-FACT ANC mode set (Off / ANC("Active") / Transparency ("Aware") / Adaptive) confirmed over the official Fast Pair Message Stream (DLCI 0x04, Group0x08, ADR-009) — i.e.libmaestro's own DLCI-0x02 settings-write/read vocabulary independently has its own concept of the same 4 ANC states (plusUNKNOWN), under a Google-internal name (qhs/ANC_STATE_*) unrelated to the public Fast Pair spec's own bit values. -
Why this is 🟡 HYPOTHESIS, not 🟢 FACT, for wire meaning: this establishes that the code contains an
ANC_STATE-typed field reachable fromWriteSetting's request/ReadSetting's response — it does not establish that DLCI 0x02 traffic is ever observed setting/reading it, nor which of fields 9/10/13 (three separate proto field numbers, all the same enum type) corresponds to which actual behavior (e.g. one could be a write-request field, another a read-response mirror, another something else entirely — the sameqhrclass is reused for bothWriteSettingrequests andReadSetting/SubscribeToSettingsChangesresponses viaqjc/qja, so field role is not determined by class identity alone). No claim is made here about why there are three, or which (if any) is the one a UI ANC-mode change would populate. -
Hypothesis test: capture an isolated ANC-mode change (repeat of
CAP-006's isolated-tap methodology) with full DLCI 0x02 traffic retained, and check whether any Sent frame'spw_hdlcpayload decodes (per thefield5{field4{...}}/qjc.field4→qhr.fieldNstructure above) to aqhrfield-9, field-10, or field-13 write carrying a value 1–4. A positive match — especially one time-correlated with the tap, mirroringCAP-006's DLCI-0x04 confirmation methodology — would be strong evidence DLCI 0x02 also carries ANC state (parallel to, not instead of, DLCI 0x04's already- confirmed path); a clean negative result across several isolated taps would suggest theseqhrfields are written by some other trigger (e.g. periodic state sync, OOBE) or an entirely different UI action. -
Update (2026-08-30, follow-up pass) — field 9 vs. 10 vs. 13 resolved for the write direction; 9/10 confirmed present-but-unhandled on the read direction. 🟢 FACT (code existence/call-site tracing across the full decompiled tree, not a wire capture): every
qhr.a.k()builder-construction call site in this APK version (grep -rl "qhr\.a\.k()" .→fye.java,fyo.java,fyp.java,hnz.java,hgj.java, plus one further site infxf.java— 6 files, all individually read this pass) was enumerated, and field 13 is the only one of the threeANC_STATE-typed oneof cases (9, 10, 13) ever populated by aWriteSettingrequest anywhere in this app version's code. No call site anywhere setsqhr.bto9or10.- Field 13's write path — two distinct, independently-traceable callers, both routing through the
same
fye.a(qhs)method (defpackage/fye.java:17-39: setsqhrVar.b = 13; qhrVar.c = Integer.valueOf(qhsVar.f), wraps intoqjcVar.b = 4; qjcVar.c = qhrVar2, sends viafyv.c(qjc)→ the already-documentedWriteSettingRPC send path):- In-app UI:
com/google/android/apps/wearables/maestro/companion/ui/quickactions/ QuickActionsFragment.java:33-98— the "Quick actions" screen's ANC toggle-groupOnClickListener. Reads the toggle group's selected index (QuickActionsFragment.java:38), maps it through a lookup table to aqhsvalue (:52,qhs b2 = qhs.b(...)), guards againstANC_STATE_UNKNOWN(:53), then calls((fye) p.get()).a(b2)(:92) — a direct, unambiguous in-app UI action (the on-screen Off/ANC/Transparency/Adaptive toggle group). - Physical earbud gesture:
defpackage/gvi.java:19-33(anmbdcallback), constructed and invoked fromdefpackage/gvj.java:105-110.gvjimplementsAudioManager .OnAudioFocusChangeListenerand reacts toqhpgesture-action-type events (qhp.java:SINGLE_TAP/DOUBLE_TAP/TRIPLE_TAP/HOLD/SWIPE_FORWARD/SWIPE_BACKWARD/SWIPE_UP/SWIPE_DOWN/HOTWORD/LEFT_ON_HEAD/etc.) viaswitch (qhpVar.ordinal())— case 4 corresponds toqhp.HOLD(Javaenum.ordinal()position,HOLDbeing the 5th-declared constant — not to be confused withqhp's own proto wire valuer, which is also 4 forHOLDby coincidence in this enum). On aHOLDgesture,gvj.java:107callsnew gvi(gvjVar), whoses()callback (gvi.java:19-33) reads the current ANC state and callsfye.a(qhs.ANC_STATE_ACTIVE)orfye.a(qhs.ANC_STATE_AWARE)— a binary Active↔Aware toggle — i.e. a physical press-and-hold gesture on the earbud also writes field 13, through the exact same code path as the in-app toggle, not a separate field.
- In-app UI:
- Read direction —
defpackage/fxb.java, methoda(mzq)(internally logged/labeled"handleAllegroSetting"; takes aqja-typed response, matchingqja's already-documentedReadSetting/SubscribeToSettingsChanges-response role; constructed with the app's own device-repository (gea) and analytics logger (ght)): aswitchover everyqhr.boneof case number from the incoming response, with named per-case handling for 2, 3, 4, 5, 7, 11, 12, 13, 15, 16, 17, 18, 19, 21, 22, 23, 27, 28, 29 — and one shareddefaultbranch (fxb.java:106-123, log message"Receive unhandled value case: %s") covering cases 6, 8, 9, 10, 14, 20, 24, 25, 26. Cases 9 and 10 fall into that same unhandled default, exactly like 6/8/14/20/24-26 — i.e. the response parser recognizes them as structurally valid oneof alternatives (consistent withqgx.j's validity table already uniformly covering fields 9/10/13, per the original pass) but takes no action on their value in this app version. Field 13, by contrast, has a dedicatedcase 13(fxb.java:278-311) that decodes theqhsenum, forwards it to the device-repository (geaVar2.I(str5, fxa.a.apply(qhsVar))), and logs it to the app's analytics pipeline ("Log ANC settings to Clearcut: %s") — i.e. field 13 is confirmed as both the write field and its own read/subscribe mirror. - Conclusion (🟡 HYPOTHESIS, strong — code-level, not capture-correlated): fields 9 and 10 are
not decoder artifacts — they are real, distinctly-numbered oneof alternatives of the same
qhsenum type as field 13 — but in this app version they are write-silent (nothing ever constructs aqhrwith case 9 or 10) and read-inert (the response handler explicitly discards them via the same generic path as several confirmed-unused fields). This narrows, rather than replaces, this entry's original "why 🟡 HYPOTHESIS" framing above: field 13 is now code-confirmed as the sole active write+read-mirror field; why fields 9/10 exist at all in the schema (an earlier/newer protocol revision, a per-earbud Left/Right split later consolidated to one field, a debug/engineering-build-only path sharing this generated schema, etc.) remains genuinely open — no evidence either way was found this pass, and none is claimed. - Hypothesis test (sharpened): same method as the original entry — a capture correlating an
isolated ANC-mode change, now specifically both an in-app tap and, separately, a physical
press-and-hold gesture — against DLCI 0x02 Sent frames. The refined prediction: only field 13
should ever appear with a nonzero
ANC_STATEvalue in a Sent frame from this app version; fields 9/10 should never appear as the populated oneof case, since no code path writes them. A capture showing 9 or 10 populated would contradict this static analysis and should be flagged as either a different app version/client, or a write call site this search missed. - Update (2026-08-30, Tier 0 re-decode task) — fields 4, 7, 12, and 29 (a distinct set from the
9/10/13 question above) independently byte-confirmed against existing captures; field 13 itself
not yet re-tested this pass (no isolated-ANC-tap-plus-gesture capture exists yet — this remains
the still-open hypothesis test above). 🟢 FACT (mechanical byte decode, re-extracted from the
raw logs):
CAP-020frames 1741/1935 decompose exactly toqhrfield 4 (value1) and field 29 (value2) respectively — see theqjc/qjaentry's own 2026-08-30 update for the full byte trace.CAP-021frames 1895/3619/4315/4976 (HOLD-001–HOLD-004) all decompose toqhrfield 7, and frames 5237/5247/5255 (HOLD-005) toqhrfield 12 — see the updatedqjo/qjuentry below for the full trace, including a previously-undocumented extra nesting level (qik→qho) inside field 7's own body. All four of these field numbers match this table's own write-call-site register exactly (field 4 = touch/head-gestures toggle perfyo.java:124-144, field 7 =qju, field 12 =qht, field 29 = head gestures) — a second, independent (wire-level) confirmation path for entries this pass had previously only established via static call-site tracing. Promoted toPROTOCOL.md§4.5.3 as 🟢 FACT, 2026-08-30, maintainer sign-off (DECISIONS.mdADR-019): field 4 (full identity) and field 7/qju(full identity, plus theqik→qhonesting correction) in full; field 12/qhtfor its field-number identity only — the maintainer reviewed and explicitly declined to promoteqht's "ANC gesture loop" name as equivalent toPROTOCOL.md§4.5.3's separate "ANC-mode rotation checklist" HYPOTHESIS. Field 29 was not included in this promotion (no self-describing code-side name exists for it, only the wire-level match) — it remains 🟡 HYPOTHESIS, unchanged.
- Field 13's write path — two distinct, independently-traceable callers, both routing through the
same
-
Bonus finding — comprehensive
qhrfield-number → UI-action register (this pass), beyond the original 9/10/13 question. Tracing everyqhr.a.k()builder call site (write direction) andfxb.java's full responseswitch(read direction) together resolved many more of the 38 fields' actual UI roles than this pass originally set out to find — included here since it fell directly out of the same investigation and is squarely stillqhr-scoped, not a drift into the unrelated 807-class sweep. 🟡 HYPOTHESIS (strong) throughout — code-level call-site/log-message evidence, not capture-correlated, per the same standard as the rest of this entry:Field Type Write call site Read case ( fxb.java) / log messagePlausible role 2 BOOL fyo.java:169-188(l)case 2 ( :52-59), no distinct logOn-Head/In-ear Detection — category-level identity confirmed 2026-09-08, via MaestroDeviceSettingsProviderServicecase2102's internal category logCATEGORY_OHD(see that entry's update)3 BOOL fyo.java:212-232(n)case 3 ( :60-78),"Log OOBE Is Finished setting"OOBE (out-of-box-experience) completion flag 4 BOOL fyo.java:124-144(h)case 4 ( :79-97),"Log Gestures Enable setting"Head/touch gestures master enable toggle 5 BOOL fyo.java:102-122(f)case 5 ( :98-105), no distinct lognot independently named 6 BOOL fyo.java:191-210(m) — found 2026-09-08, corrects this row's prior "not found" entry; own caller not located (checkedfyb's cases 0-18 andhqy.java, neither reaches.m())unhandled (default) write site exists; still read-inert and un-triggered from any found caller 7 MESSAGE→ qjufyo.java:300-374(t(gdx))case 7 ( :124-219),"Log Gestures Customization for touch and hold setting, left: %s, right: %s"touch-and-hold gesture customization, Left/Right — see updated qjuentry below8 BOOL not found unhandled (default) write-silent, read-inert 9 ENUM qhsnot found unhandled (default) write-silent, read-inert — see main finding above 10 ENUM qhsnot found unhandled (default) write-silent, read-inert — see main finding above 11 BOOL fyo.java:146-166(j)case 11 ( :220-227), no distinct logMultipoint — confirmed (self-describing log one layer up, at the ViewModel: hiy.java:32's"Set device Multipoint as: %s", reached fromMultipointFragment's toggle — see 2026-09-08 update below)12 MESSAGE→ qhthgj.java:216-331(ANC gesture-loop preference screen)case 12 ( :228-277),"Log ANC gesture loop to Clearcut"ANC gesture-loop membership (On/Off/Transparency/Adaptive) — see updated qjg/qhtentry below13 ENUM qhsfye.java:17-39(2 callers, see main finding above)case 13 ( :278-311),"Log ANC settings to Clearcut"ANC state (Off/Active/Aware/Adaptive) — confirmed 14 BOOL fyo.java:234-254(o)unhandled (default) write-only; response ignored 15 BOOL fyo.java:168-188→u(fyo.java:376-396)case 15 ( :312-319), no distinct logVolume EQ — confirmed (self-describing log + literal preference-key string one layer up: hlv.java:2127's"Set volume eq: %s", gated onstr.equals("volume_eq_switch")— see 2026-09-08 update below)16 MESSAGE→ qjwfyp.java:301-322(f, "update user eq")case 16 ( :320-323),"received user eq setting value"live/current user EQ curve — see updated qjwentry below17 SINT32(per this entry's own field register above, line 856)fxf.java:82-133(case 16 of that dispatcher)case 17 ( :324-327),"received last saved volume balance setting value"volume balance 18 MESSAGE→ qjwfyp.java:270-294(d, "update last saved user eq"; also persists to localSharedPreferenceskeykey_user_custom_eq)case 18 ( :328-331),"received last saved user eq setting value"last-saved/persisted user EQ curve — see updated qjwentry below19 BOOL fyo.java:278-298(s); alsofxf.java:113-133(volume-balance-extremity side effect)case 19 ( :332-340),"received mono setting value"Mono audio 21 BOOL hey.java:165-190(HearingWellnessFragmenttoggle)case 21 ( :341-351), no distinct logHearingWellnessFragment-scoped toggle, not further named 22 BOOL hnz.java:29-49(a,"Set Speech Detection")case 22 ( :352-354)Conversation Detection — matches PROTOCOL.md§4.5.1's existing field-22 HYPOTHESIS23 MESSAGE→ qhq(trivial marker)not found case 23 ( :355-357), callsgeaVar.H(...)not independently named 27 BOOL fyo.java:80-100(e)case 27 ( :358-361),"received case earcon setting value"case-sound toggle (matches §4.5's "Case sound" grouping) 28 BOOL fyo.java:58-78(d)case 28 ( :362-365),"received bud return sound setting value""bud return" case-sound toggle (matches §4.5's other Case-sound entry) 29 ENUM ( qgx.n/similar)not found case 29 ( :366-373)not independently named 32 BOOL fyo.java:257-276(r) — added 2026-09-08, reached fromMaestroDeviceSettingsProviderServicecase2116only (fyc.i(new fyb(z,6))→fybcase 6 →fya.r)unhandled (default) logged internal category CATEGORY_RV_BLOCK_AUTO_TEST(fjm.H(24)) — diagnostic/auto-test-sounding, not a recognizable user-facing featureFields left out of this table (2, 5, 11, 15, 21, 29 excepted where partially covered above) had a write and/or read site but no distinguishing log message or UI-fragment context traced this pass; included in the table only where at least one side (write call site, read log message, or both) gave a nameable role. Fields 1, 6, 8, 14, 17 (write side), 20, 24, 25, 26, 30-38 were not traced to any call site this pass (either genuinely absent from the app's current write paths, like 6/8/9/10, or simply not searched for — this table is not claimed exhaustive of all 38 fields).
-
Open questions: what fields 24/26/29's enum types actually are (validity-checker delegates to an unnamed obfuscated helper, unlike
qhs); what the remaining nestedMESSAGEfields' own semantics are beyond their raw shape (see their individual entries below, several now resolved per the table above); why fields 9/10 exist in the schema at all, given they are confirmed write-silent/read-inert in this app version (see the 2026-08-30 update above — this replaces the original phrasing of this question, which asked only "why 3 field numbers", now narrowed to "why 2 apparently-unused ones"). -
Update (2026-08-30, Tier 2 follow-up pass) — fields 24/26/29's delegate chain traced one level further; a dead end, not a resolution. 🟢 FACT (code reading,
qgx.javain full +a.java'saO/aImethods):qgx.n/qgx.r/qgx.s(the three delegate instances this entry's fields 24/26/29 route through, per the original pass) areqgx's own indices 13, 17, and 18 respectively — and all three call the exact same underlying method,a.aO(int)(a.java:487-489:return aI(i) != 0;), which itself delegates toa.aI(int)(a.java:431-439: a 3-value remap,{0→1, 1→2, 2→3, else→0}). This is the same generic 3-valued validity/remap helper already seen elsewhere in this document'sqjn/qjtwrite-side entry (fyw.java'sqjg-field boolean-to-tri- state conversion, e.g.i21 = ((qjgVar.b & 1)==0||!qjgVar.c) ? 2 : 3) — i.e.qgx.n/r/sare not per-field-type enum validators; they are three call sites of one shared, generic "is this a valid 3-state ordinal" helper, reused across unrelated proto enum types by R8 (matching the already-documented pattern fora.at/a.aselsewhere in this codebase). Tracing this delegate chain one level deeper does not reveal fields 24/26/29's actual backing enum type or semantic meaning — it confirms they share a shape (a 3-valued enum, structurally like a tri-state boolean) but nothing more. This is a genuine dead end via this specific path, not merely "not attempted" — recorded as such rather than left ambiguous, per this document's own zero-creativity standard. Fields 24/26/29 remain unnamed. -
Update (2026-09-03, maintainer-approved field promotions) — fields 17, 19, 22, 27, and 28 from the bonus register above (2026-08-30 pass) promoted in
PROTOCOL.md/DECISIONS.mdADR-019, applying this entry's own evidence-path standard (wire capture + independently-traced, self-describing app code). 🟢 FACT (mechanical byte decode, re-extracted fromCAP-022/CAP-024's raw logs, plus the same static call-site tracing already used above):- Field 17 ("volume balance") — full identity promoted.
CAP-022frames 1922/1944/2019/2039/2056/2073/2099, CRC-32 verified, decode toqhrfield 17 across a continuous drag gesture. This entry's own field register (line 856) already types field 17 asSINT32; the bonus table above previously mistyped it as "INT32/BOOL-adjacent" (corrected in place this session) — re-reading the 7 sampled wire values as zigzag-encodedSINT32(not raw unsigned varints) gives-100, -62, -25, 15, 75, 100, 5(previously199, 123, 49, 30, 150, 200, 10). Combined with the self-describing read-side log"received last saved volume balance setting value"(fxb.javacase 17), promoted toPROTOCOL.md§4.5.7 in full. - Field 19 ("Mono audio") — full identity promoted.
CAP-022frames 1621/1823 (both directions), CRC-32 verified, decode toqhrfield 19. Self-describing read-side log"received mono setting value"(fxb.javacase 19) matches this entry's own "Mono audio" reading exactly. Promoted to a newPROTOCOL.md§4.5.5a in full. Field 19 has two independent write paths to the same field, not one: (a) the dedicated Mono-audio toggle (fyo.java:278-298,s(boolean)) — this field's established, primary identity, unchanged by the below; and (b)fxf.java:113-133's volume-balance-extreme-value side effect (part of the same case-16 dispatcher that writes field 17, line 999 above) — a genuine secondary write to field 19, triggered when an extreme balance value is set, not an alternative identity for the field. Both write sites were independently re-confirmed maintainer-side (ai-sessions/ 0008_CROSSCHECK_RESULT_2026_09_11.md§1.18/§1.22); "Mono audio" remains field 19's sole primary role. - Field 22 ("Speech Detection") — field-number/type identity only promoted.
CAP-019frame 1808 decodes toqhrfield 22. The write-side log is self-describing (hnz.java's"Set Speech Detection"), but that name is not the same string asPROTOCOL.md§4.5.1's pre-existing "Conversation Detection" UI-label HYPOTHESIS — the maintainer reviewed this specifically and declined to promote that equivalence, matching how field 12's "ANC gesture loop" name was handled above. Promoted toPROTOCOL.md§4.5.1 for field-number/type identity only; the "Conversation Detection" label equivalence remains 🟡 HYPOTHESIS. - Field 27 (case-sound category) — category-level identity only promoted.
CAP-024frames 2053/2084 (both directions), CRC-32 verified, decode toqhrfield 27. The read-side log,"received case earcon setting value"(fxb.javacase 27), is self-describing enough to confirm a real case-sound-family boolean, but is generic — it does not itself name "Other alerts" specifically. Promoted toPROTOCOL.md§4.5.8 for category-level identity only; the specific "Other alerts"/"Other notifications" label remains 🟡 HYPOTHESIS. - Field 28 ("Bud return") — full identity promoted.
CAP-024frames 1988/2023 decode toqhrfield 28. Self-describing read-side log"received bud return sound setting value"(fxb.javacase 28) matches this entry's own "Bud return"/"Earbuds replaced" reading exactly. Promoted toPROTOCOL.md§4.5.8 in full.
Fields 11 (Multipoint) and 15 (Volume EQ) from
PROTOCOL.md§6's same open item were not checked against the recoveredqhrschema this pass and remain 🟡 HYPOTHESIS, unaffected. - Field 17 ("volume balance") — full identity promoted.
-
Update (2026-09-08,
ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.mdPhase 2, maintainer sign-off obtained via prompt0002) — fields 11 (Multipoint) and 15 (Volume EQ) closed, via a forward trace from a named UI fragment/preference key to the write call site, rather than the backward-from-a-log-message technique used for the fields above. 🟢 FACT (code call-graph tracing, re-verified this session):- Field 11 = Multipoint — full trace, self-describing log message found one layer above
fxb.java's response handler (which itself has no distinct log for case 11):com/google/android/apps/wearables/maestro/companion/ui/settings/multipoint/MultipointFragment.java(a genuinely unobfuscated class name) binds itskey_multipoint_main_toggleMainSwitchPreference'sOnCheckedChangeListener(cq(CompoundButton, boolean), line 71-75) tohiy.a(boolean)(hiy.java:31-38), which logs"Set device Multipoint as: %s"(hiy.java:32) before calling((fyc) g.get()).e(z)(hiy.java:37).fyc.e(boolean)(fyc.java:return i(new fyb(z, 9));) dispatches throughfyb's case 9 (fyb.java:66-70:((fya) obj).j(this.a)) — i.e.fya.j(boolean), which isfyo.j(boolean)(fyo.java:146-166), the exact call site already identified above as writingqhr.b = 11. Full chain, UI to wire field:MultipointFragment(toggle:key_multipoint_main_toggle) →hiy.a(z)("Set device Multipoint as: %s") →fyc.e(z)→fyb(z,9)→fya.j(z)=fyo.j(z)→qhrfield 11. - Field 15 = Volume EQ — full trace, with a literal Android preference-key string match, a
stronger evidence type than any
qhrfield promoted so far:defpackage/hlv.java:2125-2134(a large R8-merged preference-click dispatcher,e(Preference)) has a case gated onstr.equals("volume_eq_switch")— the literal Settings-XML preference key for the "Volume EQ" toggle at the bottom of Device details → Sound → Equalizer (PROTOCOL.md§4.5.6's own UI description) — logging"Set volume eq: %s"(hlv.java:2127) before calling((fyc) g2.get()).h(z2)(hlv.java:2133).fyc.h(boolean)(fyc.java:return i(new fyb(z, 7));) dispatches throughfyb's case 7 (fyb.java:56-60:((fya) obj).u(this.a)) — i.e.fya.u(boolean)=fyo.u(boolean)(fyo.java:376-396), the exact call site already identified as writingqhr.b = 15. Full chain:hlv's preference-click handler (key:"volume_eq_switch") →"Set volume eq: %s"log →fyc.h(z)→fyb(z,7)→fya.u(z)=fyo.u(z)→qhrfield 15. - Bonus, adjacent finding (not part of this task's ask, found in the same dispatcher): the case
immediately before Volume EQ's,
str.equals("loudness_comp_switch")("Loudness compensation"), logs"Set loudness compensation: %s"and calls((fyc) g.get()).i(new fyb(z, 5))→fyb's case 5 →fya.i(boolean).fyo.i(boolean)(fyo.java:411-412) is an empty no-op method body — on this app version's Pixel-Buds-Pro-2-specific schema (fyoimplementsfya), toggling "Loudness compensation" produces noqhrwrite at all; this method is presumably only implemented by one of the siblingfyw/fyxclasses for a different product. Not one ofPROTOCOL.md's documented Buds Pro 2 features — recorded here as a genuine negative result (a real, named Sound settings toggle that is wired in the shared UI code but produces zero wire traffic for this device), not chased further. - Evidentiary strength, stated precisely: both traces combine (a) an exact,
independently-verified call-graph path with no ambiguous branch, and (b) a self-describing
string — a log message for field 11, and both a log message and the literal Android
preference-key string for field 15. This meets or exceeds the evidence bar this entry's own
2026-09-03 update used for its already-promoted fields (which relied on the read-side
fxb.javalog message alone in most cases). Promoted toPROTOCOL.md§4.5.2 (Multipoint, field 11) and §4.5.6 (Volume EQ, field 15) as 🟢 FACT in full, 2026-09-08, maintainer sign-off (prompt0002,DECISIONS.mdADR-025's 2026-09-08 Update note) — both readings match the pre-existing wire-derived HYPOTHESIS labels exactly, unlike fields 12/22/27 which kept a naming equivalence unpromoted.
- Field 11 = Multipoint — full trace, self-describing log message found one layer above
defpackage.qjn / defpackage.qjt / defpackage.qhx / defpackage.qjv — qjc/qja's other 4 oneof-group alternatives
-
Path:
qjn.java:28(field 2 ofqjc/qja),qjt.java:28(field 3),qhx.java:28(field 1),qjv.java:28(field 5) — same directory as above. -
Readable alias: MaestroSettingGroup2 / MaestroSettingGroup3 / MaestroSettingGroup1 / MaestroSettingGroup5
-
Role: 🟢 FACT (code existence/structure only), each a single oneof like
qhr, but far smaller:qhx(Group 1): 1 field — field 1, plainBOOL. The simplest of the 5 groups; plausibly a single-flag category, not otherwise identified this pass.qjn(Group 2): 12 fields, numbers 1–12. Fields 1,2,3,5,6,7,8,10,11 =BOOL; field 4 =MESSAGE(qjo, 2-field message-of-messages, see below); field 9 =MESSAGE(qjg, 4×BOOL, see below); field 12 =ENUM(type reached viaqgx.o→case 14: return qic.a(i2) != 0;— delegates to another unnamed obfuscated helper, not independently identified).qjt(Group 3): 10 fields at sparse numbers {1,3,5,7,8,10,11,12,13,14} (2,4,6,9 unused/ reserved — not explained by this pass). Fields 1,3,5,7,8,10,11,13 =BOOL; field 12 =ENUM(sameqgx.oreference asqjnfield 12 — i.e.qjnfield 12 andqjtfield 12 share the exact same enum type, worth noting for anyone pursuing this further); field 14 =INT32.qjv(Group 5): 3 fields. Field 1 =MESSAGE(qit, trivial 0-field marker type — see below); field 2 = plainBOOL; field 3 =MESSAGE(qjd, also a trivial 0-field marker type).
-
Open questions: no field in
qjn/qjt/qhx/qjv(unlikeqhr'sANC_STATEfields) resolved to an unobfuscated, self-describing name this pass — all remain unidentified as to which app feature they belong to. None of the 4 groups' own field numbering/count was cross-checked againstPROTOCOL.md§4.5.1–§4.5.8's already-HYPOTHESIS individual settings list (Conversation Detection, Multipoint, Touch controls, Head gestures, press-and-hold ×4, Mono audio, Volume balance, In-ear detection, Case-sound toggles ×2) — a follow-up pass matching those to specific field numbers across all 5 groups (61 total top-level fields: 1+12+10+38+3 = 64, minusqjt's 4 unused numbers = 60 actually-present) would be a natural next step, but was not attempted here to avoid guessing field semantics from count/position alone. -
Update (2026-08-30, Tier 2 follow-up pass) — a structural reframing, not just more field names:
qhx/qjn/qjtare very likely each an alternate, per-product-variant settings schema, not 3 of 5 "categories" within one product's own settings. 🟢 FACT (code existence/structure and call-graph tracing across the write-side classes): every write call site forqhr's fields is indefpackage/fyo.java, a class thatimplements fyadirectly. Every write call site forqjn(this Group 2) andqhx's field is indefpackage/fyw.java, whichextends fxz implements fya— a different, sibling class. Every write call site forqjt(this Group 3) is indefpackage/fyx.java, which alsoextends fxz.fyo,fyw, andfyxare the only three classes in this APK that implement thefyainterface (grep -rln "implements fya\|extends fxz" .→fxz.java,fyo.java,fyw.java,fyx.java— 4 files, one of which,fxz, is only ever a base class, never instantiated directly by any DI-provider site found this pass), and each is constructed from a distinct dependency-injection provider case:frg.java:244(new fyo(...), feedingqhr's writes) vs.fub.java:52/:54(new fyw(...)/new fyx(...), feedingqjn's/qjt's writes respectively) — three separate construction sites, not one class picking a branch at runtime. Crucially,fyoandfyw/fyximplement almost entirely disjoint subsets offya's methods, each stubbing the other's methods as empty no-ops inherited fromfxz's or their own synthetic overrides (e.g.fyo.c/.g/.i/.k/.p/.q/.v/.ware all empty stubs infyo.java:402-432, whilefywgives every one of those a realqjn/qjg-writing body). This is the standard shape of "one interface, N alternate implementations selected per hardware/product variant at DI-wiring time," not "one implementation handling 5 sub-categories of the same product's settings." Reading this project's own already-confirmed context onto it:qhr(Group 4, reached viafyo) is independently confirmed, by this project's own wire-capture evidence (PROTOCOL.md§4.1/§4.2/§4.5, all captured against the maintainer's actual Pixel Buds Pro 2 unit), to be the schema the Buds Pro 2 actually uses.qhx/qjn/qjt(reached viafyw/fyx) are therefore a strong 🟡 HYPOTHESIS (structural, not wire-confirmed): alternate settings schemas for other Google earbud products sharing this same companion app andWriteSettingRPC method — plausibly an older/different Pixel Buds generation, or another "Presto"-family device (see the literal internal codename found below) — not the hardware this project targets. If correct, most of the field names recovered below describe a different device's settings, not gaps in the Buds Pro 2's own feature set; perPROJECT.md's non-goal ("no support for other Pixel Buds models unless the protocol is demonstrably identical"), this narrows rather than expands this project's own remaining work, but is flagged prominently since it changes how every finding below should be read. Not independently confirmed: no capture evidence (this project has none targeting a different earbud model) checks which ofqhx/qjn/qjta different physical device would actually select — this reading rests entirely on the DI/method-overlap structure, not a wire observation. -
qhx(Group 1) resolved — OOBE (out-of-box-experience) mode toggle, both directions. 🟡 HYPOTHESIS (strong — code-level, both write and read sides self-describing): the only call sites referencingqhxoutside its own oneof-schema declarations aredefpackage/fxz.java(write) anddefpackage/gaa.java(read). Write —fxz.m(boolean z)(fxz.java:20-44, the base class's own override, inherited by everyfyaimplementor that doesn't overridem()itself —fyw/fyxboth inherit it unchanged): logs"Change oobe mode state to %s"(fxz.java:23), buildsqhxfield 1 = the boolean, wraps it asqjcfield 1 (qhx's own oneof case), and sends it via the standardWriteSettingpath — and additionally, in the same call, sends a second, separate write (qixwithqixVar.c=1, method id 8) alongside it, a detail not decoded further here (out of this pass's scope). Read —defpackage/gaa.java, methodb(int i, mxr mxrVar)(a large notification dispatcher distinct from the already-documentedfxb.java, which handlesqhr/Group 4 specifically —gaa.bhandlesi==10, decoding aqjaresponse and switching on its own oneof case): case 1 (qhx) logs"Oobe mode is on"(gaa.java:405) when the boolean is true, and falls through to anACTION_TYPE_UNKNOWNgesture dispatch otherwise (gaa.java:407) — i.e. OOBE mode being on appears to gate/suppress normal gesture-action dispatch, consistent with an out-of-box-experience flow wanting to intercept gestures. This class-level identification (OOBE mode) matches, by label and by shared code pattern (a single boolean field, no distinguishing suffix), several other "OOBE"-named fields found inqjn/qjtbelow (field 8, "oobe finished state") — these are plausibly related but are distinct fields in distinct oneof groups, not the same field seen twice; no evidence this pass ties them together beyond the shared word "oobe" in their respective log messages. -
qjn(Group 2) — resolved field-by-field for 8 of its 12 fields, both write and read sides largely self-describing; the class's own internal name is literally "presto." 🟡 HYPOTHESIS (strong — code-level; not capture-correlated, since no capture in this project's possession exercises a different-product Buds unit):defpackage/gaa.java's read-side dispatcher (same method asqhxabove) logs, for this oneof case specifically,"Value field of presto setting not set"(gaa.java, default branch of the Group-2 switch) — "presto" is this app's own internal codename for whatever productqjnbelongs to (not this project's own prior "presto"/"MaestroSettingGroup2" alias, which was a project-invented placeholder — this is the app's own string). Write side (defpackage/fyw.java, all methods overriding the sharedfyainterface):Field Type Write call site ( fyw.java)Read case ( gaa.java) / log messagePlausible role 1 BOOL l(),:163-175,"Change OHD state to %s"case i15==1, no distinct log"OHD state" — plausibly On-Head-Detection; note qhrfield 2 (this app's other, Buds-Pro-2 schema) is independently wire-confirmed as In-ear detection (PROTOCOL.md§4.5.5) and is also reached via the same shared interface methodl()infyo.java:169-188— i.e.l()is this app's own "wear-detection toggle" slot across both schemas, under two different internal names (seePROTOCOL.mdcross-reference note below)4 MESSAGE→ qjot(gdx),:227-350,"Touch control has no value to set"(guard message when empty)case i15==4, builds agdxand callsgeaVar2.ad(...)Left/Right "touch control" gesture-action pair — see the updated qjo/qjuentry below; structurally and functionally parallel toqju/qhrfield 7 (also reached via the same shared interface methodt(gdx), infyo.java, for the Buds-Pro-2 schema)5 BOOL k(),:148-160,"Change noise detection state to %s"case i15==5, no distinct log"noise detection state" 6 BOOL p(),:193-205,"Change shared mode state to %s"case i15==6, no distinct log"shared mode state" — plausibly an audio-sharing/multi-listener feature; not this project's own already-confirmed Multipoint ( PROTOCOL.md§4.5.2, which isqhrfield 11, a different oneof group entirely)8 BOOL n(),:178-190,"Change oobe finished state to %s"case i15==8, no distinct log"oobe finished state" 9 MESSAGE→ qjgc()/g()/q()/y()(4 separate one-flag-at-a-time setters,:82-99,:116-133,:207-224,:370-380)case i15==9"Attention alert" (4-boolean group) — see the updated qjg/qhtentry below10 BOOL f(),:101-114,"Change diagnostics state to %s"case i15==10, no distinct log"diagnostics state" 11 BOOL h(),:135-145, no distinct log either sidecase i15==11, no distinct lognot independently named 12 ENUM ( qic, viaqgx.o)w(gdc),:352-368,"Change Eq Setting state to %s"case i15==12, routes throughgeaVar3.S(str3, fms.D(i2))"Eq Setting" — an enum-valued EQ setting (plausibly a preset selector), structurally distinct from qhr's own EQ representation (qjw, a 5×floatcustom-curve quintet, fields 16/18) — consistent withqjnbelonging to a different, likely simpler product that only supports EQ presets, not a 5-band custom curve.qic's own 3 constants (qic.a/.b/.c= 1/2/3) are themselves unnamed integers, not self-describing likeqhs'sANC_STATE_*— which specific preset/value each represents is not resolved.Fields 2, 3, 7 were not traced to a call site this pass (no
fyw.javamethod maps to them in the file as read); field 12'sqgx.odelegate (index 14,case 14: return qic.a(i2) != 0;) is the same delegateqjtfield 12 uses below (already noted in the original pass) — now additionally confirmed to route, on both sides, through the exact samefms.D(qic)/gea.S(...)conversion and setter, reinforcing (not merely coincidentally) thatqjnfield 12 andqjtfield 12 represent the same underlying "Eq Setting" concept for their respective product/schema. -
qjt(Group 3) — resolved for 6 of its 10 fields by direct cross-reference againstqjn's newly- named fields, via the same sharedfyainterface methods;qjt's own write class (fyx.java) carries no distinct log messages of its own. 🟡 HYPOTHESIS (strong for the field-number identification, since it's a direct code cross-reference; slightly weaker for the semantic name itself, sinceqjt's own code path never logs it — the name is inherited fromqjn's sibling method, not independently stated):defpackage/fyx.java(extends fxz implements fya, the same sibling relationship asfyw) implements the same interface method names asfyw, writing toqjtinstead ofqjn:Field Type Write call site ( fyx.java)Shared fyamethodName inherited from qjn's same method1 BOOL l(),:117-127l(boolean)"OHD state" (matches qjnfield 1)5 BOOL k(),:105-115k(boolean)"noise detection state" (matches qjnfield 5)8 BOOL n(),:129-139n(boolean)"oobe finished state" (matches qjnfield 8)10 BOOL f(),:69-79f(boolean)"diagnostics state" (matches qjnfield 10)11 BOOL h(),:81-91h(boolean)not independently named (matches qjnfield 11, also unnamed)12 ENUM (same qgx.o/qicasqjnfield 12)w(gdc),:153-168w(gdc)"Eq Setting" (matches qjnfield 12 exactly — same delegate)13 BOOL i(),:93-103i(boolean)not independently named — qjnhas no field usingi()(fywdoes not override it; inherited empty stub fromfxz), so there is no sibling name to borrow14 INT32 v(int),:141-151v(int)not independently named — same situation as field 13, qjnhas no field usingv()This cross-reference method (matching two sibling classes' shared interface method names, not their field numbers or log text directly) is a new technique introduced this pass — it works here specifically because
fyw/fyxare structurally parallel siblings of the same base class (fxz) and interface (fya), and is a genuine confirmation (not a guess) for the 6 fields with aqjn-side log message to borrow, but is explicitly not claimed for fields 13/14, where no sibling name exists to cross-reference and no name is invented.qjt's own fields 3 and 7 (present per the original pass's sparse-numbering list) were not traced to a call site this pass —fyx.javahas no method settingqjtVar.bto 3 or 7. -
qjv(Group 5) confirmed fully unused in this app version — both write-silent and read-inert, for the entire group, not just some fields within it. 🟢 FACT (exhaustive whole-tree text search, not merely "no call site found in the files searched" as the original pass's phrasing put it):grep -rn "qjv\b" .across every.javafile injadx-output/sources/returns exactly 5 hits —qjv.javaitself,qjc.java/qja.java(the two oneof-schema declarations, already documented),qix.java:28(an unrelated oneof's own info-string, which separately references theqjvclass as one of its own nested types — a coincidental reuse of the class, not aqjc/qja-oneof write path), anddefpackage/fyv.java:68— the central outbound-write dispatch/deduplication method (fyv.c(qjc), the function everyfyaimplementor's write path funnels through) has a defensivecaseforqjc.b==5(computing a dispatch-map key from((qjv) qjcVar.c).b), but this is dead code from a construction standpoint: no site anywhere in the decompiled tree ever builds aqjcwith case 5 set (unlike cases 1/2/3/4, each of which has a real, traced builder call site above). On the read side,gaa.java's response dispatcher (documented above) explicitlyreturns without any action for oneof case 5, the same as case 4 (qhr, correctly — that one's handled by the separatefxb.javaclass instead) — but unlike case 4, no other class anywhere handles case 5 either. Conclusion:qjv(and by extension its own field 1/qitand field 3/qjdtrivial marker types) is present in the schema but entirely inert in this app version — a group-level parallel toqhr's individual fields 9/10, but total rather than partial.
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/qjw.java:32 -
Readable alias: MaestroFloatQuintet
-
Role: 🟢 FACT (code existence/structure): 5 plain
FLOATfields (proto numbers 1–5, Java fieldsc,d,e,f,g, each with its own hasbit/presence). This is the same shape (5 floats) as the already-🟢-FACT EQ band quintet (PROTOCOL.md§4.2,ADR-016: Low bass/Bass/Mid/Treble/Upper treble, wire order reverse of on-screen order) confirmed over DLCI 0x02'sfield5{field4{...}}envelope via capture (CAP-015).qjwis referenced from two separate field numbers insideqhr: field 16 and field 18 (bothONEOF(MESSAGE) ref=qjw.classin the decoder output) — this is a direct, code-level structural match toPROTOCOL.md§4.2's own already-recorded open question: "EQ's outer field 16 vs. 18 ('preview' vs. 'fires on slider-release') reading remains 🟡 HYPOTHESIS". This pass did not have that field-16-vs-18 question in mind when decodingqhr— it fell out of the mechanical decode — which makes the match a genuine independent corroboration (static structure now shows both candidate field numbers really do exist, both as the identical 5-float message type) rather than a search targeted at confirming a preexisting guess. -
What this does NOT establish: which of field 16 / field 18 is "preview" and which is "commit on release" (or whether that framing is even correct) — this pass only confirms both fields exist and share
qjw's shape; it does not decode which is which, since that requires wire-level timing evidencePROTOCOL.md§4.2 already says is still missing. -
Hypothesis test: exactly the one
PROTOCOL.md§4.2 already names — a capture with the EQ slider dragged and released, distinguishing intermediate-drag DLCI 0x02 traffic from the final release write, then checking which field number (16 or 18) each maps to. This finding does not change what evidence is needed, only confirms both candidate field numbers structurally exist. -
Update (2026-08-30, follow-up pass) — fields 16 and 18 both resolved to a specific role (though not to "preview" vs. "commit-on-release" specifically). 🟡 HYPOTHESIS (strong — code-level, not capture-correlated, so this does not by itself answer
PROTOCOL.md§4.2's preview-vs-commit timing question):defpackage/fyp.javaimplements the EQ-write interface (fyd) with two distinct methods, each with its own explanatory log message, both confirmed independently on the read side bydefpackage/fxb.java's response handler:- Field 16 = "user eq" (current/live curve):
fyp.java:301-322(methodf(qjw)) logs"update user eq: %s"(:302) and sendsqhrfield 16. Read side:fxb.javacase 16(:320-323) logs"received user eq setting value".f()is called both frome(gdy)(fyp.java:296-299, itself called whenever the EQ curve object changes) and, viac(int)(fyp.java:264-268,"setPreset: %d"), whenever a built-in EQ preset is selected. - Field 18 = "last saved user eq" (persisted curve):
fyp.java:270-294(methodd(gdy)) logs"update last saved user eq: %s"(:272), sendsqhrfield 18, and — uniquely among the two — also persists the same value locally, independent of the device round-trip:this.e.a.edit().putString("key_user_custom_eq", Base64.encodeToString(gdyVar.f(), 0)).apply()(fyp.java:293, an AndroidSharedPreferenceswrite). Read side:fxb.javacase 18(:328-331) logs"received last saved user eq setting value". - Reading: this establishes field 16 as "the EQ curve currently in effect" and field 18 as "the
curve to remember/restore" (with a local
SharedPreferencescache backing the latter specifically) — a live-value/persisted-value split, not necessarily the in-drag-preview/release-commit splitPROTOCOL.md§4.2 hypothesized. The two readings are compatible (a "commit on release" write could plausibly update both the live value and the persisted one, explaining whyd()/field 18 is the one with the extraSharedPreferencesside effect) but this pass found no call site whered()andf()are invoked together or in a fixed sequence from one user gesture —d(gdy)ande(gdy)→f(qjw)are two separate, independently-callable interface methods (fyd.d/fyd.eper the interface each implements), and no caller of either was traced this pass (out of scope — would require findingfyd's call sites in the EQ UI fragment, not attempted here). The preview-vs-commit timing questionPROTOCOL.md§4.2 asks remains open; this only replaces "field 16 and 18 are structurally identical, role unknown" with "field 16 and 18 have distinct, named roles (live vs. persisted), whose relationship to slider-drag-vs-release timing is still untraced."
- Field 16 = "user eq" (current/live curve):
-
Update (2026-08-30, Tier 0 re-decode task) — the "live"/"persisted" reading checked against
CAP-005's andCAP-015's existing captured sequences; consistent with, and sharpens, the wire-observed pattern, without fully resolving the drag-vs-release timing question. 🟡 HYPOTHESIS (strengthened by correlation, not independently capture-verified from scratch — this re-reads already-published decodes fromCAP-005-FINDINGS.md§5a andCAP-015-FINDINGS.md§4, cross-checked againstfyp.java's code, not a fresh re-extraction from the raw logs):- Preset selection (
fyp.c(int)→f(qjw)= field 16 only, perfyp.java:264-268/:301-322) predicts presets should only ever produce field-16 writes, never field 18.CAP-015's own 6-preset sequence (frames 2111/2165/2227/2303/2351/2400,CAP-015-FINDINGS.md§4's table) matches this exactly — every preset tap in that capture is field 16, zero field-18 frames appear anywhere near a preset tap. This is a clean, exact match between the code-derived call graph (presets route throughf()/field-16 only) and the wire-observed pattern (presets are field-16-only in both captures that exercise them,CAP-005andCAP-015). - Slider drags predict continuous field-16 writes (from
e(gdy)→f(qjw), called "whenever the curve object changes") plus a separate field-18 write fromd(gdy)at some other trigger.CAP-015's 50 slider-related frames match this shape exactly: for every one of the 15 drag-cycles, a run of field-16 frames (one per intermediate value) is followed by exactly one field-18 frame, always carrying the identical final value to the immediately-preceding field-16 frame — consistent withd(gdy)/field-18 firing once per gesture, not continuously, and never introducing a new value of its own (matchingfyp.java's own description ofd()'s side effect: persisting the current value locally, not computing a different one). - Does not resolve which specific UI event calls
d(gdy).CAP-015-FINDINGS.md§6 already found field 18 fires 0.05–1.9s after the last field-16 write in every cycle, with no video-visibleSave-button tap in between, and revised its own hypothesis to "field 18 fires on slider-release" — this pass's code reading is compatible with that (aSeekBar.OnStopTrackingTouch-style listener plausibly calling bothe()→f()during the drag andd()once at release), but no call site forfyd.d/fyd.e(the interface methodsfypimplements) was traced this pass to confirm it — the same gap this document's own originalqjwentry already flagged. Net effect of this correlation: the "field 16 = live, field 18 = persisted-locally" code-level reading and the "field 18 fires on release, not on Save" wire-level reading are mutually consistent and reinforce each other, but neither independently confirms the other's specific causal claim (what code callsd(), vs. what UI gesture the wire's timing implies) — both remain 🟡 HYPOTHESIS, now on two convergent evidence paths instead of one.
- Preset selection (
-
Update (2026-09-08,
ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 3 item 4) —fyd.dandfyd.e's call sites in the EQ UI fragment (UserEqFragment.java) now traced fully; the result is a genuine tension with the 2026-08-18 capture's "fires on slider-release" revised hypothesis, not a confirmation of it. 🟢 FACT (code call-graph tracing, self-describing log messages at both ends of each path):fyd.e(gdy)(field 16, "live") is fed directly and only from the slider-drag/preset-selection path:UserEqFragment.java:294-300'saC(Preference, Object)-style callback (fired on every slider value change and preset selection) callshomVar.m.f(gdyVar11)—homVar.mis apfz(RxJava-Subject-like) field wired, inhom.java's constructor (hom.java:77), through a 150ms debounce and acombineLatestwith the device-connection stream, intohnv(2)/ggb(case 3)'s subscriber body, which is exactlyfyd.e(gdy). This confirms field 16 fires reactively on every debounced slider-drag tick and on preset selection — matching the existing "live/current curve" reading, now with the actual UI trigger identified for the first time.fyd.d(gdy)(field 18, "persisted") is gated behind an explicit, dedicated "Save" button — not slider-release.hom.java's constructor (:78) wires a second, separatepfzfield (this.t, private) as an RxJava "sample" trigger: whenthis.temits, the latest value from acombineLatestof the device stream is passed throughhoq(this,1)'s body (hoq.java, case 1), which — after checking device-info/EQ-controller availability, logging"Saving user EQ: %s"— calls exactlyfyd.d(gdy).this.tis emitted into by exactly one method,hom.k()(hom.java:132-138:this.t.f(true); return true;), andhom.k()'s sole caller in the entire decompiled tree ishju.javacase 19 (hju.java:208-216), the click handler wired (UserEqFragment.java:457-479) to a real, namedButtonPreference/MaterialButton— string resourceskey_eq_save_button/title_eq_save_button— logging the self-describing"On click save EQ button"before callinghomVar.k(), and on success showing atoast_message_eq_savedtoast (UserEqFragment.aK()). No other call site forhomVar.k(), and no lifecycle hook (onPause/onStop/onDestroy) calling it either, was found anywhere in the tree.- This is a genuine, unreconciled tension with the wire-observed timing, not a confirmation of
the "slider-release" hypothesis. The code shows field 18/
fyd.dis reachable only through an explicit Save-button tap — there is no code path from slider-release (finger lift) tod(gdy)anywhere in this app version's decompiled source. This directly contradictsCAP-015-FINDINGS.md§6's own revised reading (that field 18 fires on slider-release with "no video-visibleSave-button tap in between") for every one of that capture's 15 drag-cycles. Per this project's zero-creativity rule, this is recorded as an open tension, not resolved by guessing which side is wrong — plausible (unconfirmed) explanations include:CAP-015's own video may have missed a genuinely-fast/off-screen Save-button tap between each drag-release and the next action; the Save button may auto-fire under some UI state this pass didn't identify; or an app-version difference betweenCAP-015's capture date and this pass's analyzed APK version. None of these is asserted — only that the code, as read, shows no other path to field 18. - Correction (2026-09-13,
ai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.mdPhase 1 re-verification) — the "reachable only through a dedicated Save-button handler" claim above is factually incomplete: a genuine second call site tohom.k()(and therefore tofyd.d/field 18) exists, not found by the 2026-09-08 pass. 🟢 FACT (code existence, self-describing log message):hod.java:36— a second, distinct lambda (hod,implements ebe) also callshomVar.k(), gated oncrmVar.d() != null && ((hpp) crmVar.d()).b(a boolean, plausibly an "unsaved changes" flag —hom.java:128setshpp's second boolean field from a diff-equality check,hoj.a(...) == -1, consistent with but not confirmed as a dirty-flag), and logging the self-describing"Navigate away, save EQ"immediately before callinguserEqFragment.aK()(the same save-confirmation toast method the Save-button path also calls). Thishodinstance is registered inUserEqFragment.java:82(private final ebe am = new hod(this, 0);) and wired viabvl.n(this).g(this.am)(UserEqFragment.java:556) — an event-bus-shaped registration (ebo's constructor computes a string key fromecfVar.getClass()), not aSeekBar/slider-touch listener and not confirmed as a Jetpack Navigation destination-changed callback either; exactly what event class triggershod.a(ebo)was not traced this pass (out of this bounded re-verification's scope). The 2026-09-08 entry's own "sole caller in the entire decompiled tree" claim is corrected here, not silently left standing, per this project's zero-creativity rule — a targeted cross-reference of every file holding ahom-typed field against every.k()call site found exactly these two call sites (hju.javacase 19,hod.java:36), no others. - What this does NOT establish: whether this second, "navigate away" trigger is what actually
fired in
CAP-015's 15 drag-cycles (still no call-site trace connects either path to a literal slider-release/finger-lift gesture) — it adds a third candidate trigger (Save button / navigate-away-with-unsaved-changes / the still-unconfirmed slider-release reading) rather than resolving which one explains the wire timing. A capture isolating all three conditions from each other (not tapping Save, not navigating away, and genuinely only releasing the slider) is the only way to close this — designed asCAPTURE_BLUETOOTH_HCI_SNOOP.mdGroup AO (plannedCAP-053), seeai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.mdPhase 1. - This closes
TODO.md's own item ("tracefyd.d/fyd.e's own call sites... to connectqjwfield 16/18's code-derived reading to the wire-observed drag/release timing") with a concrete, code-level answer — but the answer sharpens the open question into a real contradiction rather than resolving it. Directly relevant toDECISIONS.mdADR-020's own scope note that the field-16-vs-18 semantics should be resolved before EQ ships a "Save as preset" UI affordance — this finding suggests that affordance's design should not assume field 18 fires on slider-release, contrary to the capture's own prior working hypothesis.
- Path:
qjg.java:31(referenced fromqjnfield 9),qht.java:31(referenced fromqhrfield 12) - Readable alias: MaestroBoolQuad
- Role: 🟢 FACT (code existence/structure): both are 4-field messages, each field a plain
BOOLwith its own hasbit (Java fieldsc,d,e,f). ⚪ ASSUMPTION (naming/count-based, not confirmed): the 4-boolean shape is at least consistent withPROTOCOL.md§4.5.4's already-better-evidenced press-and-hold finding (4/4 Left/Right × ANC/Assistant combinations,ADR-013) — 4 independent booleans is exactly the shape 4 combinations of a 2×2 matrix would take if each combination is its own flag rather than 2 separate 2-way enums. Not claimed as confirmation; two different message types (qjg,qht) share this identical shape, so shape alone does not distinguish which (if either) is press-and-hold versus some unrelated 4-flag setting. - Update (2026-08-30, follow-up pass) —
qht(theqhrfield-12 alternative) resolved;qjgremains open. 🟡 HYPOTHESIS (strong — code-level, not capture-correlated):qht's 4 booleans (Java fieldsc,d,e,f) are set one-per-ANC-state indefpackage/hgj.java:216-331, the UI fragment behindR.xml.anc_preference(hgj.java:53-54) — a settings screen with 4CheckBoxPreferences keyedanc_preference_key_on/_adaptive/_off/_txp(hgj.java:115-129). Each checkbox's on-change handler builds aqhtwith exactly one of its 4 fields set (qhtVar.c=On/qhtVar.d=Off/qhtVar.e=Transparency/qhtVar.f=Adaptive,hgj.java:224-330), wraps it asqhrfield 12, and sends it as aWriteSetting. The read side confirms the same shape and adds an explicit name:defpackage/fxb.java's response handler,case 12(fxb.java:228-277), unpacks all 4 booleans and logs"Log ANC gesture loop to Clearcut: %s"(:267) — i.e.qht/qhrfield 12 is the "ANC gesture loop" feature: which ANC states (On/Off/Transparency/Adaptive) are included when press-and-hold cycles through ANC modes (thehgj.javaUI screen for configuring that cycle's membership, as opposed tofye/qhrfield 13, which is the actual state-change command the cycle — or the in-app toggle group — sends). This is a different, related feature fromADR-013's "press-and-hold ×4 Left/Right × ANC/Assistant combinations" reading of DLCI 0x02 traffic — that capture-derived finding and this code-derived one have not been reconciled; they may describe the same underlying setting seen from two angles, or two genuinely different settings that happen to share a 4-flag shape.qjg(theqjnfield-9 alternative, a different top-level oneof group fromqht's) was not resolved this pass — no write or read call site for it was found in the files searched. - Open questions: which (if either) of
qjg/qhtisADR-013's press-and-hold finding —qhtnow has its own, better-evidenced "ANC gesture loop" identity (above), which may or may not be the same thingADR-013observed on the wire; whatqjgis remains fully open. - Update (2026-08-30, Tier 2 follow-up pass) —
qjgresolved: "Attention alert," both directions. 🟡 HYPOTHESIS (strong — code-level, self-describing on the read side):qjgis theqjn(Group 2, see that entry's 2026-08-30 update above) field-9 alternative, not aqhrfield — a correction to this entry's own original framing, which listedqjg/qhtside-by-side as if both wereqhr-reachable; onlyqhtis (qhrfield 12). Write —defpackage/fyw.javaimplements 4 separatefyainterface methods, each toggling exactly one ofqjg's 4 booleans and sending the result:c(z)sets fieldc(bit 1),g(z)sets fielde(bit 4),q(z)sets fieldf(bit 8),y()sets fieldc=falseunconditionally (bit 1, no parameter — an explicit "turn off" action distinct fromc(z)'s general setter). Read —defpackage/gaa.java's same Group-2 response dispatcher (seeqjn's entry above), case 8 (i15==9): unpacks all 4 booleans into agcsobject, callsthis.b.J(this.e, gcsVar5)(a device-repository setter), then separately checks whether all 4 fields now read a specific "all off" pattern (a.aI(...)tri-state remap,==2for each — see the qhr-fields-24/26/29 update above for whata.aIdoes) and, if so, logs"Turn off attention alert"and fires an app-internal event (((fyc) this.c.a()).i(new fmp(16)).p()). This is the first unobfuscated, self-describing name forqjg: a 4-flag "Attention alert" feature, distinct in both shape and now-confirmed identity fromqht's "ANC gesture loop" (also 4 booleans, but a differentqjn-sibling schema's field,qhrfield 12, with its own separate "Log ANC gesture loop to Clearcut" read-side name) — the two are not the same feature despite the shared 4-bool shape, resolving this entry's own "shape alone does not distinguish which (if either)" caveat forqjgspecifically (it does still apply to any other untraced 4-bool message this project might encounter later). Not capture-correlated —qjgbelongs to theqjn/"presto" schema, which per theqjnentry's structural finding is very likely a different product than this project's own Buds Pro 2, so no capture in this project's possession is expected to exercise it at all.
defpackage.qjo / defpackage.qju — 2×MESSAGE wrapper messages (possible Left/Right or two-part containers)
- Path:
qjo.java:29(referenced fromqjnfield 4),qju.java:29(referenced fromqhrfield 7) - Readable alias: MaestroPairWrapper
- Role: 🟢 FACT (code existence/structure): both are 2-field messages where each field is itself
a nested
MESSAGEwith its own hasbit/presence (Java fieldsc,d) — i.e. a small container holding two independently-optional sub-messages, not two scalars. The nested message classes themselves were not traced this pass (not visible in the decoder's summary output beyond "MESSAGE"; a full run would needobjects-array class-reference resolution one level deeper, not attempted here to control scope). ⚪ ASSUMPTION: the two-slot shape is consistent with a Left/Right earbud pair container, but equally consistent with any other 2-part structure (e.g. "current"/"target", or two unrelated sub-features) — not distinguished by structure alone. - Update (2026-08-30, follow-up pass) —
qju(theqhrfield-7 alternative) resolved with a specific name;qjoremains open. 🟡 HYPOTHESIS (strong — code-level, not capture-correlated):defpackage/fxb.java's response handler,case 7(fxb.java:124-219), unpacksqju's two nestedqikmessages (fieldsc/d, each itself wrapping aqhovalue viafzw.a(qhoVar)) and passes the two decoded values through a helperb(int)(fxb.java:19-25, mapping to anfphenum:UNDEFINED/NONE/ANC/TALK_TO_ASSISTANT) before logging"Log Gestures Customization for touch and hold setting, left: %s, right: %s"(fxb.java:193) — i.e.qju/qhrfield 7 is the Left/Right press-and-hold gesture action customization (which of ANC/Assistant/None each earbud's press-and-hold triggers), matchingPROTOCOL.md§4.5.4/ADR-013's already-documented "press-and-hold ×4 Left/Right × ANC/Assistant combinations" finding by name, not just by shape — this is the first code-level confirmation of which field carries that setting. The write side (defpackage/fyo.java:300-374, methodt(gdx gdxVar)) builds the matchingqju/qik/qhostructure from agdx(Left/Right gesture-action pair) and sends it asqhrfield 7.qjo(theqjnfield-4 alternative, a different top-level oneof group) was not resolved this pass — no write or read call site for it was found in the files searched. - Open questions: what
qjo's own two nested message types are —qju's Left/Right press-and-hold-gesture-action reading is no longer speculative (see update above), but nothing established this pass carries over toqjo, a structurally similar but functionally untraced type. - Update (2026-08-30, Tier 2 follow-up pass) —
qjoresolved: Left/Right "touch control" gesture-action pair, structurally and functionally parallel toqju, for theqjn/"presto" schema instead ofqhr. 🟡 HYPOTHESIS (strong — code-level, both write and read sides traced):qjois theqjn(Group 2) field-4 alternative (see that entry's own 2026-08-30 update). Write —defpackage/fyw.java, methodt(gdx gdxVar)(:227-350) — the same interface method name and samegdxdomain-object parameter type asfyo.java's already-documentedt(gdx)forqju/qhrfield 7. Logs"Touch control has no value to set"(fyw.java:230) as a guard when the incominggdxhas neither Left nor Right set, then builds aqjk(notqik—qjo's own nested type is spelled differently fromqju'sqik, confirmed by direct read of both files, not a typo) with two fields (c=Left,d=Right per the(gdxVar.b & 1)/(gdxVar.b & 2)bit checks matchingfyo's identical pattern forqik), each itself wrapping aqhovalue via the samefzw.b(int)/fzw.a (qho)helper pairqjuuses, and wraps the result asqjnfield 4. Read —defpackage/gaa.java(same Group-2 dispatcher asqjn's other fields), case 4 (i15==4): unpacksqjoVar.c/.d(qjk's own twoqho-wrapped values, viafzw.a) into agde/gdxpair — the exact same domain types (gde,gdx)fxb.javabuilds forqju's read side — and callsgeaVar2.ad(str2, (gdx) k.q()). What this establishes:qjoisqjn's own Left/Right press-and-hold-style gesture- action container — the write-side log's literal wording, "touch control", is the closest this pass found to a UI-facing name (contrastqju's own read-side log,"Log Gestures Customization for touch and hold setting, left: %s, right: %s"— both describe the same broad feature area, touch/ hold gesture-action assignment, under each schema's own wording). Per theqjnentry's structural finding, this is very likely the other product's equivalent of the Buds Pro 2's own press-and-hold feature (qju), not a second, additional feature on the Buds Pro 2 itself — not capture-correlated, for the same reason given there.qjo's inner type is namedqjk(notqik) — this document's original framing ofqjo/qjuas sharing an identical, unnamed nested-type shape is confirmed structurally (both wrap 2qho-typed values) but the concrete class names differ per schema, now recorded rather than left implicit. - Update (2026-08-30, Tier 0 re-decode task) — byte-level capture confirmation of
qju's (notqjo's — no capture exercises theqjn/"presto" schema) own nested structure, and a nesting detail finer than this document's original shape description. 🟢 FACT (mechanical byte decode, re-extracted fromCAP-021-btsnoop_hci.log, cross-checked againstCAP-021-FINDINGS.md§3's own citation and found byte-identical): frames 1895/3619/4315/4976 (HOLD-002/HOLD-004/HOLD-001/HOLD-003) all decompose, after the standardfield5{field4{...}}outer wrapper, toqhrfield 7 (qju) — matching this entry's own write-site identification (fyo.java:300-374) exactly. One level finer thanCAP-021-FINDINGS.md's own published decode ("field7(len6){ field1| field2(len4){ field4=varint(5|6) } }" — a simplification that treats the innerfield4as a direct varint): the actual bytes showfield4is wiretype 2 (length-delimited), not a bare varint — e.g. frame 1895'sqju.field1(Left) body22 02 08 06decomposes as tag0x22= field 4, wiretype 2, length 2, containing08 06= tag0x08= field 1, wiretype 0, value6. This matches this document's own already-establishedqjushape exactly:qju.field1(Left)→qik(a length-delimited submessage, not a scalar) →qik.field4→qho(itself length-delimited) →qho.field1= the raw integer (5=Active noise control /6=Digital assistant). All 4HOLD-001–HOLD-004frames decode cleanly through this 3-level nesting (qju→qik→qho) with the predicted Left/Right selector (field1/field2) and value (5/6) matchingCAP-021-FINDINGS.md§3's own table exactly. Separately, frames 5237/5247/5255 (HOLD-005) decompose toqhrfield 12 (qht) with exactly 4 boolean sub-fields (tags0x08/0x10/0x18/0x20, one varint each) — matchingqht's already-documented 4×BOOLshape exactly, no further nesting (unlikeqju,qht's own fields are plain booleans, notqik/qho-wrapped). SeeCAP-021-FINDINGS.md's 2026-08-30 addendum for the full byte trace and reproduction script. Promoted toPROTOCOL.md§4.5.3 as 🟢 FACT, 2026-08-30, maintainer sign-off (DECISIONS.mdADR-019):qju/field 7 in full, including theqik→qhonesting correction;qht/field 12 for its field-number identity only, not its "ANC gesture loop" name's equivalence to the separate rotation-checklist HYPOTHESIS.
defpackage.qhq / defpackage.qiq / defpackage.qjf / defpackage.qis / defpackage.qit / defpackage.qjd — trivial 0-field marker types (action-trigger candidates)
- Path:
qhq.java:26,qiq.java:26,qjf.java:26,qis.java:26(all referenced asMESSAGEalternatives insideqhr, fields 23/31/35/37 respectively),qit.java:26,qjd.java:26(referenced fromqjvfields 1/3) - Readable alias: MaestroEmptyMarker (6 distinct classes, same trivial shape as the
already-documented
nia/qib) - Role: 🟢 FACT: each decodes to 0 real fields (matching
nia/qib's info-string shape,\u0001\u0000-style), i.e. structurally an "empty"/marker message — a value whose presence (this oneof alternative being the one populated) is the entire signal, no payload. ⚪ ASSUMPTION: plausibly action-trigger-style settings (e.g. "force X now", akin toForceMultipointSwitch's request shape) rather than value-carrying settings, given the shape — not confirmed. - Open questions: which specific action, if any, each corresponds to.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/qjb.java:31 - Readable alias: MaestroSoftwareInfoResponse
- Role: 🟢 FACT: named as
GetSoftwareInfo's response type infux.java:57(nqs.c("GetSoftwareInfo", (mzw) niaVar4.a(7, null), (mzw) qjb.a.a(7, null))) and matches the typefxm.java's response handler actually casts to (fxk.java:23:qjb qjbVar = (qjb) mzqVar;). Decodes to 4 fields: field 3 =ONEOF(MESSAGE, qjj.class), field 4 =ONEOF(MESSAGE, qie.class)(both alternatives of one oneof), field 5 = plainFIXED64(Java fieldd), field 6 = plainBOOL(Java fielde). Cross-check:fxk.java:30readsif (qjbVar.e) { return; }on the very response object this type describes — the decoder's field 6 → Java fielde→BOOLmatches this live usage exactly, which is direct evidence (not just decoder self-consistency) that the decode is correct. ⚪ ASSUMPTION: field 6/eis read as a "stop here, don't treat as primary route" flag in context (fxk.java:30-35: skips promoting this route to primary ifeis true) — plausibly an "is this response from a secondary/non-primary core" flag, not independently confirmed as such beyond that one call site's control-flow behavior. - Open questions:
qjj/qie(the two oneof alternatives) and field 5'sFIXED64value were not traced further this pass. - Update (2026-09-13,
ai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.mdPhase 5) —qie's own shape decoded (scripts/decode_rawmessageinfo.py), and compared byte-for-byte againstCAP-036frame 1423's actual wire content; the existing "structurally matchesqie" reading inPROTOCOL.md§6 is corrected, not confirmed, by this pass. 🟢 FACT (mechanical decode + direct byte-level comparison, command+hex shown in the RESULT file above):qiedecodes to 3 fields (1/2/3), each typedMESSAGE(Java fields c/d/e) — notSTRING.CAP-036frame 1423's own connect-time-burst sub-message (the pw_rpc payload field, 48 bytes) decodes cleanly and exhaustively as a 3-field message with fields 1/2/3, each a direct, length-14STRING(0a 0e <14 bytes "57071WRBEC0251">,12 0e <...>,1a 0e <...>) — no additional nested tag+length exists between each field's own length-delimiter and its string bytes, which aMESSAGE-typed field would require. This is a structural non-match againstqie, not a match: aqieinstance's own 3 fields would each need to look like0a NN 0a MM <string bytes>(an extra nested message wrapper), not the flat0a 0e <string bytes>actually observed. By contrast,qjm/qjr(qiv's/GetHardwareInfo's own two oneof alternatives, decoded this same pass) are each exactly a 3-field, fields-1/2/3, all-STRINGmessage — matching frame 1423's observed shape exactly, both in field numbering and field typing. What this does NOT establish: RPC identity (no service/method identifier was decoded this pass — the outer pw_rpc/pw_hdlcenvelope bytes surrounding this 48-byte payload were not fully parsed, only the payload's own field structure) — this is a structural-shape comparison only, the same evidentiary tier the original 2026-09-08 finding used, now pointing the other way. Proposed forPROTOCOL.md§6 as a correction, pending review; a live correlation capture (Group AS, plannedCAP-057) is the recommended way to settle RPC identity directly.
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/ghd.java:37(alsoghb.java:116) - Readable alias: MaestroDynamicServerConfigRpcClient
- Role: 🟢 FACT: defines/calls the
maestro_pw.DynamicServerConfigService/SetConfigpw_rpc unary call. - Update (2026-08-30, follow-up pass) —
ghb.javatraced in full; confirmsfwv/npy/nqoare shared, service-agnosticpw_rpcplumbing, notWriteSetting-specific. Found while tracingdefpackage/fwy.java's other factory cases for context (fwyis confirmed a 20-case, R8-merged generic Dagger-style factory implementingnpi— same pattern asfrg/gau/fsz/fuu; case 6 (fwy.java:53-54) is the one that constructsghb:new ghb((oql) this.c.a(), (ffd) this.d.a(), (fwv) this.e.a(), (Map) this.a.a(), (fvl) this.b.a())).ghb implements fuc(fuc.java:1-4, a single-method interfaceoqh a()). Its two real methods,b(pip)andc(Map, pip)(ghb.java:48-133/155-252), are Kotlinsuspendfunctions — JADX shows their coroutine state-machine bytecode rather than clean source, but the raw dump is legible enough to trace:ghb.b(pip)(ghb.java:87-120, reading the raw bytecode) does, in order:fwv r7 = r6.e; oql r7 = r7.a(); oqz r7 = r7.Z(); ... r7 = ple.A(r7, r0)— i.e. it callsfwv.a()(the exact same single-method interfacefwv.java:1-4,oql a(), thatfut— the already-documented HDLC/pw_rpc RFCOMM connection,class fut extends gbd implements fwv— implements) and awaits the result before doing anything else. It then similarly awaitsfvl.a(a second gate/lookup table), and finally:Map r6 = r6.f; ... Object r6 = r6.get(r0); npy r6 = (npy) r6; ... int r7 = ((Number) r7).intValue(); ... nqo r6 = r6.f(r7, "maestro_pw.DynamicServerConfigService", "SetConfig"); return r6;— the exact samenpy.f(int channelId, String service, String method)→nqo(MethodClient) call already fully documented in thenqx/npy/nqo/npw/nqmentry above forWriteSetting, here invoked forDynamicServerConfigService/SetConfiginstead. This directly confirmsfwv.a()is a generic "wait until the pw_rpc channel/route for thisgoqtarget is ready" gate used by more than one RPC-calling class (not something specific to theWriteSetting/ANC-settings path), and thatnpy/nqo(Client/MethodClient) are shared plumbing serving everymaestro_pw.*service infux's catalog, not infrastructure built specifically for settings writes. This generalizes, without contradicting, the earliernqx/npy/nqo/npw/nqmentry's findings — theWriteSettingchain traced there is one instance of this shared mechanism, not a separate one.ghb.c(Map, pip)(ghb.java:155-252) serializes an incomingMap<Integer, Boolean>-shaped config (filtered to keys{1, 2, 3},ghb.java:10/207-208) into a length-delimited protobuf varint-field stream (mxy, aCodedOutputStream-style writer,ghb.java:203-226: for each of the 3 allowed keys present in the map, writesfield=<key>, value=<boolean>viar2.f(r5, r6)), wraps the resulting bytes into aqibinstance viamyp.n(qib.a, bytes, ..., myd.a)(the standard protobuf-liteparseFrom(bytes)-equivalent static helper), then callsr10.e(r8)(ghb.java:237) — i.e.nqo.e(mzq), the exact same unary-RPC-invoke method already documented on theWriteSettingpath — passing the just-builtqibas the request payload. Caveat:qibwas previously documented (this doc's Tooling-note section) as one of two known-trivial 0-field marker types theRawMessageInfodecoder script was validated against — this call site's use ofqibas a byte-carrying request wrapper is not necessarily a contradiction (constructing a message viaparseFrom(bytes)doesn't require the target type to have any named fields; the raw bytes could simply become unrecognized/extension fields on an otherwise-empty message, or this could be a different, R8-merged-under-the-same-nameqibthan the one previously decoded) — not independently resolved this pass, flagged rather than guessed at.
- Open questions: what "dynamic server config" refers to here (feature-flag-style remote config
pushed to the Buds themselves, vs. something purely client-side) — not yet clear from these two
files alone. New from this pass: whether the
qibghb.cconstructs from raw bytes is genuinely the same 0-fieldqibdocumented elsewhere in this doc, or a same-named-but-distinct R8-merged class — not resolved.
defpackage.gbu — KPI event collector (out of core ANC/EQ/settings scope; documented for completeness)
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/gbu.java:13-214 - Readable alias: KpiEventCollector
- Role: 🟢 FACT (code existence/structure): constructed by
defpackage/fwy.javacase 4 (fwy.java:47-50:new gbu(ftmVar, ((gib) this.e).a(), executorService, ((npm) this.a).a(), ((gkr) this.b).a())), found this pass while tracingfwy's other cases for context, per the same task that tracedghb(see theghdentry's Update above).gbu implements fzy, gak(fzy.java:1-4: single methodvoid b(int, mxr);gakhas additional methods, onlya()/b()traced).gbu.b(int i, mxr mxrVar)(gbu.java:63-188) is a dispatcher keyed on a small integeri(cases seen: 1, 4, 5) that parses incoming payloads asqkr/qkq-typed protobuf messages (gbu.java:73-188) — variable names, log strings ("Received KPI: %s","Error parsing KpiEvent","Error uncompressing Kpi events"), and the constructor's use offux.java's already-documentedcase 6(a10a20.kpi.Kpi/KpiStream— "KPI" = Key Performance Indicator) all point to this being a device-reported usage/performance-metrics event collector, not an ANC/EQ/settings feature.gbu.g()(gbu.java:36-43) sends a Group137/Code2request viathis.d.l(137, 2, f, j2)—this.disftm-typed, a facade whosel(int, int, byte[], long)signature mirrorsgbd.d(int, int, byte[])'s own Group/Code/Value write shape (plus a 4th rate-limiting/delay argument) — i.e. this feature also rides the same generic Group/Code-addressed RFCOMM transport asgbd's base implementation, just through anftmwrapper layer not otherwise traced this pass. Decoded/decompressed KPI events are forwarded to agbvinstance (this.h, not traced) rather than sent anywhere off-device in this class itself. - What this does NOT establish (superseded in part by the Update below): whether these KPI events
are ever transmitted off-device once collected here —
AGENTS.md§1's "Zero-GMS"/no-telemetry ban is, and remains, a rule for this project's own implementation, not a claim about what the official app does; the finding below documents the official app's behavior only, purely as informational background, per this doc's existing convention of describing reference-app behavior without implying it as a model to follow. - Update (2026-08-30, follow-up pass) — traced
gbv/gbt/gbs/ghq/gicend-to-end: KPI events are assembled into a protobuf analytics-log entry and handed to a class whose own log tag is literally"ClearcutEventLog", gated by a per-device consent check. 🟢 FACT (code existence/structure — the"ClearcutEventLog"string is about as strong non-capture evidence for this claim as static analysis gets; this describes the official Google app only, see the caveat above):defpackage/gbv.java(final class gbv extends gbx) is a small, generic batching/dedup helper — its 5 bridge methods (a–e,gbv.java:6-44) keyqkrKPI events by destination (qkmenum, viaa()), compute a time-delta (b()), merge/re-timestamp duplicates (c()), and filter (d()/e()) — i.e.gbxis a generic batch-and-flush base class,gbvis itsqkr-specific instantiation. Neithergbvnorgbxthemselves send anything anywhere; they hand off to whatevergbw-typed callback the base class's flush logic was given.defpackage/gbt.java(final class gbt implements gbw, constructed fresh per flush asnew gbt(this)fromgbu) is that callback — its single methoda(Object obj)(gbt.java:16-224,obj= one flushedqkrKPI event) is where the event actually gets turned into an analytics-log entry:- Builds an
nhminstance (gbt.java:21-65: fieldse=destination enum,f=an int,h=an enum-minus-1 value,g=a timestamp delta) — this is the first external reference found tonhm, previously flagged in this doc's "807-class sweep, unidentified" table as "richest schema found in the entire sweep (76 fields, 0 oneofs); no externalnew nhm(/nhm.classreference found" — that open item is now resolved:nhmis a per-KPI-event detail record. When the event carries extra structured payload bytes ((qkrVar.b & 16) != 0 && qkrVar.g.d() > 0,gbt.java:63-65), decoding is delegated togbs.a(myk, qkr)(defpackage/gbs.java:1758, the last and, per a quick shape check, only substantial method in that 1765-line file —gbsis a single-purpose class, not an R8-merged multi-case dispatcher likefwy/gau/frg, unlike this pass's working assumption going in — it appears to be one large per-KPI-event-type payload decoder, not independently traced further this pass given its size and this task's bounded scope). - Wraps the
nhminto anerinstance (gbt.java:67-86: fielde=thenhm, fieldf=gbuVar.e— an in-memory incrementing per-collector sequence counter) and logs it locally first:((lsx) ((lsx) gbu.a.c()).R((char) 370)).s("KPI event: %s", nhmVar5);. - Builds a second, richer
ner(gbt.java:96-219) carrying:d=aneq(a single string field,String.valueOf(giaVar.a)— an app-instance/session identifier, not a device MAC),c=anepcomposed of 3neomessages, each populated from agdd.cstring pulled off agdm"device metadata" object (gbt.java:119-165— 3 identifying strings, not decoded further; plausibly device model/variant/serial-class fields given the surroundinggcl/device-registry context),g=a timestamp (gcl.d()), and two enum-derived intsh/ifromgic.b(gdh)applied togcl.h()/gcl.i()(gic.b,gic.java:22-45, a 13-case ordinal remapping — not independently named). - Gate:
gic gicVar = gbuVar.c; String d = gbuVar.d.d(); if (gicVar.a(d)) { return; }(gbt.java:87-91) — before sending, checksgic.a(String)(gic.java:47-51:Optional e = this.a.e(str); return e.isPresent() && !((gcl) e.get()).B();) keyed by a device-identifying string.gcl.B()(gcl.java:47-55) reads a single boolean-shaped field (.A) off agdwsub-message nested inside a device descriptor (gdb), guarded by a presence-bit check ((gdwVar.b & 256) == 0) — i.e.gic.a()returns true (causinggbt.a()to return early, not sending the event) when the device is known and that one boolean field is false — structurally consistent with (not independently confirmed as) a per-device "diagnostics/usage-data sharing" consent flag stored in device metadata. - Sink:
ghqVar.a(l.q(), new pzm(), 0);(gbt.java:220) —ghqisgicVar.c(gic's own constructor,gic.java:16-19:this.c = new ghq(context, hvrVar);).defpackage/ghq.java:9:public static final lud a = lud.m("ClearcutEventLog");— the class's own logger is tagged, verbatim,"ClearcutEventLog".ghq.a(mzq, iml, int)(ghq.java:16-23) callshvrVar.h(mzqVar, imx.a(this.c, imlVar)).g(i).c().k(...)— a build-event/set-code/enqueue-and-callback chain matching the real Clearcut client API shape.defpackage/hvr.java:9:public final class hvr extends hvm— a real (non-R8-merged) class hierarchy, consistent with wrapping an actual Clearcut logging library client (package name itself obfuscated, not independently confirmed ascom.google.android.libraries.clearcut.*or similar beyond the log-tag string).
- Builds an
- Reading: KPI events collected from the Buds over Bluetooth (per
gbu's already-documented Group137/Code2request/response cycle) are batched, converted into annhm/ner/nep/neoanalytics-log-entry protobuf structure carrying device-identifying metadata, and — unless a per-device consent-adjacent flag (gcl.B()) reads false — handed to a class whose own logger tag is literally "ClearcutEventLog", i.e. the official app's Clearcut analytics pipeline. This is the same general logging surface already seen elsewhere in this project's traces (ght's many "Log ... to Clearcut" calls in the settings-write handlers, e.g.fxb.java/QuickActionsFragment .java) —ghq/hvrhere is very plausibly the same or a closely related underlying mechanism, thoughght(settings-event logging) andghq(KPI-event logging, this entry) were not cross-checked against each other for whether they share the samehvrinstance/sink this pass. - What this still does NOT establish: whether these Clearcut events actually leave the device on
the network (Clearcut's own client-side batching/upload-scheduling logic was not traced —
hvr/hvmwere not opened), and the exact real-world meaning ofgcl.B()'s flag,neq's session-identifier string, ornep/neo's 3 device-metadata strings — all read structurally, not semantically confirmed. Reiterating the caveat above: this entire finding describes the official Google app's behavior; it is not, and must not be read as, a suggestion for this project's own implementation —AGENTS.md§1's ban on any telemetry/analytics SDK in this project's own code is unaffected and unchanged by this finding.
- Open questions (partly resolved below): what
ftm/gib/npm/gkr(the remaining collaborator typesgbuitself is built from) individually are;gbs.java's own per-event-type payload decoding was not traced given its size and this task's bounded scope. Genuinely out of this project's ANC/EQ/settings-protocol scope unless a future task specifically needs it — flagged here as a plain listing per this pass's own instructions, not a relevance judgment. - Update (2026-08-30, follow-up pass) —
ghtvs.ghq'shvrsink compared: same base class/method and the same device-consent gate confirmed; likely (not fully confirmed) different named Clearcut/Streamz log sources. 🟢 FACT (code existence/structure — this describes the official app only; see the caveat above, unaffected and unchanged by this finding):- Same class hierarchy, same sink method, not overridden:
defpackage/ght.java:7:public class ght extends ghq— confirmed a direct, single-level subclass, not a parallel reimplementation.ght's own constructor (ght.java:19-29) takes its ownhvr hvrVarargument and forwards it straight intoghq's constructor (super(context, hvrVar)).ght.java(449 lines, read in full this pass) defines noa(mzq, iml, int)override — every one of its ~15 public logging methods (b/c/d/e/f/g/i/j/k/l/m/n/o/p/q/r/s,ght.java:35-419, each building anfpp/fps/fpy/fqv/fra-typed settings/usage-event protobuf and logging a"Log ... to Clearcut: ..."message — matching every already-documented "Log ... to Clearcut" call site traced in earlier passes) funnels through the single private dispatcherh(String, fpn, fpm)(ght.java:116-132), which asynchronously schedulesdefpackage/ghs.java(final class ghs implements mbd) as a callback.ghs.s(Object)(ghs.java:20-...) is where the actual send happens: after resolving device info and gating onz = ((gcl) optional.get()).B()(ghs.java, same variable name and samegcl.B()method as the KPI path's gate below), it callsghtVar.a(fpnVar, new pzl(), fpmVar.k)— sinceghtdefines noa(...)override, this resolves to the inheritedghq.a(mzq, iml, int)method, the exact same methodgbt.a()'s KPI-event path calls (asnew pzm()there vs.new pzl()here for theiml-typed 2nd argument — different log-source-tag objects, not traced further, but the method is identical). - Same gating flag:
ghs.java'sz = ((gcl) optional.get()).B()uses the exact samegcl.B()method (gcl.java:47-55, the single boolean-shaped device-metadata field already documented in thegbuentry's KPI-path finding) asgic.a(String)'s gate on the KPI path (gic.java:47-51:!((gcl) e.get()).B()) — i.e. both the settings-event and KPI-event Clearcut-logging paths are gated by the identical per-device flag, strengthening (not just repeating) the earlier reading thatgcl.B()is a shared, feature-wide diagnostics/analytics-consent flag rather than something KPI-specific. (ght's gate additionally requires!booleanValue, a secondPair-derived flag not traced this pass — plausibly a dedup/already-sent check, not confirmed.) - Likely different underlying
hvrinstances (named log sources), not fully confirmed: unlike the transport-layergbnsingleton resolved in an earlier pass (exactly onenew gbn(call site in the whole app),hvrhas multiple distinct construction sites with different 2nd-argument "log source" string tags:defpackage/ghz.java:27:new hvr(b, "A10A20_KPI", (String) f.get())— explicitly tagged"A10A20_KPI", matchingfux.java's already-documenteda10a20.kpi.Kpi/KpiStreamservice naming, and thus a strong candidate for the specifichvrfeedinggic'sghq(gib.java:33:new gia(b, intValue, gckVar, hvrVar, ...),hvrVara passed-in parameter not traced to its ultimate origin this pass, but the "A10A20" naming correlation is suggestive);defpackage/gwv.java:1626:new hvr(context, "STREAMZ_GNP_ANDROID", null)— a third, differently-tagged construction, for an apparently unrelated "Streamz" metrics pipeline (see below);defpackage/hvl.java:22:new hvr(this.a, this.b, this.d, this.c, null, null, this.e)— a generic factory whose tag argument (this.b) is parameterized per-caller, not resolved to a specific string this pass.ght's ownhvrargument (fuu.java:79:new ght(b5, intValue, hvrVar, freVar, gckVar, ftjVar, mbrVar, ...)) is likewise a passed-in parameter, not traced to its ultimate tagged-construction site this pass — whether it resolves to the same"A10A20_KPI"-tagged instance the KPI path likely uses, a fourth not-yet-found tag, or the generichvl.javafactory with some other tag, is not established. Given the KPI-specific tag naming already found, and thatght's events are semantically settings/usage events (not KPI events), the more likely reading is that they use different named log sources sharing the sameghq/hvrclient class and API, not a literal single shared instance — but this is not confirmed, only the more plausible of the two readings. - Bonus finding —
ght/ghsalso send to a second, separate pipeline, unconditionally:ghs.java's tail end (after thegcl.B()-gatedghtVar.a(...)call, and independent of it) builds anmko-typed message (ghs.java, fields including a fixedmkoVar.c = 1017code) and submits it viaghtVar.g(ght's owngwv gwvVarfield,ght.java:12) —gwvis the class whose"STREAMZ_GNP_ANDROID"-taggedhvrconstruction was found above (gwv.java:1626), i.e. this second call is very plausibly Google's internal "Streamz" metrics system, not Clearcut. Unlike theghq.a(...)call, this second send is not gated bygcl.B()/booleanValue— only byif (fpmVar.equals(fpm.UNKNOWN)) { return; }— a different, apparently-always-on telemetry path running alongside the consent-gated Clearcut one. Not traced further this pass (out of this task's bounded scope, which was specifically theght-vs-ghqcomparison), but flagged since it's a directly-adjacent, previously-unnoticed second pipeline. - Update (2026-08-30, follow-up pass) — this second pipeline traced one level further: no gating
found in the code path actually exercised, but the trail runs into another giant R8-merged class
before reaching a concrete sink, so "no gating" is confirmed only for the segment actually read,
not end-to-end. 🟢 FACT (code existence/structure only; describes the official app, unaffected
caveat as above):
defpackage/mko.javadecodes (python3 scripts/decode_rawmessageinfo.py mko.java) to exactly 3 fields —c=INT32 (the1017constant, plausibly an event/metric code),d=INT32 (fromfpm.k, the settings-event-type enum's int value),e=STRING (a device-correlated label) — a small, generic "coded counter with a label" shape, consistent with a Streamz metric cell.ghs.java:31confirmsint i = 2;is unchanged through the method — i.e.new hvn(mkoVar4, str6, i)(ghs.java:203) unambiguously invokeshvn's case 2.hvn(hvn.java:15,final /* synthetic */ class hvn implements mab) is, itself, yet another R8-merged multi-case dispatcher (9 cases, 0–8 plus a default,hvn.java:33-150) — the same pattern already seen repeatedly (fwy/gau/frg/fsz). Case 2's body (hvn.java:76-77) is exactly one line:return ((iqd) obj).a((mko) this.a, (String) this.b);— no gating check of any kind appears in this specific case, confirming last pass's suspicion for the segment of the chain visible inghs/hvnthemselves.defpackage/iqd.java(7 lines) is a bare single-method interface:mbo a(mko mkoVar, String str);— i.e. the actual sink implementation is not visible at this call site at all; it comes from whateverobjresolves to (the result ofr1.submit(new epf(obj2, 14)),ghs.java:203—r1/obj2beinggwvVar.c/gwvVar.a), cast toiqd. Tracing the concreteiqdimplementation would require openingdefpackage/epf.java's own case 14 — not done this pass (see bounded-scope note below).- Important correction to last pass's framing:
defpackage/gwv.java(1631 lines) is confirmed, not just suspected, to be another R8-merged multi-purpose class —grep -rn "new gwv(" .finds at least 5 structurally distinct constructor call sites across unrelated files (bkr.java:10:new gwv((char[]) null);bzq.java:14:new gwv(this);dbu.java:23:new gwv((Context) this.a, (byte[]) null);ddg.java:21:new gwv((byte[]) null, (byte[]) null);ddg.java:112:new gwv((Object) dkbVar, (Object) dkhVar, (Object) dddVar, (byte[]) null)— 5 incompatible signatures, an unambiguous R8 constructor-merge signature). The one constructor this pass read in full (gwv(List list),gwv.java:52-...) initializes fieldsa/b/cas a plainHashMap/HashMap/ArrayList— not anExecutor-shaped object, even thoughghs.javacallsr1.submit(...)ongwvVar.c(anExecutor-style call) — i.e. thegwv(List)constructor this pass happened to read is not the oneght's own(gwv) this.j.a()-resolved instance actually uses; a different, not-yet-located constructor overload must be the real one. The class also declarespublic static gwv d;(gwv.java:48), a classic singleton-holder field, which is at least consistent with (not proof of) one shared instance existing somewhere — but which ofgwv's many merged identities that static field belongs to was not determined. Net effect: the"STREAMZ_GNP_ANDROID"-taggedhvrconstruction found last pass atgwv.java:1626-1629is real code that exists somewhere in this file, but this pass could not confirm it belongs to the same merged-constructor identity thatght'sgwvVarfield actually resolves to at runtime — this downgrades (without retracting) last pass's "very plausibly Google's Streamz metrics system" reading from "the mechanism is identified" to "a\"STREAMZ_GNP_ANDROID\"-tagged construction exists in the same giant classghtalso draws from, not confirmed as the same code path." - Genuinely valuable side-finding, from
hvn's other cases (not the oneghs.javainvokes, found while reading the file to identify case 2): case 1 (hvn.java:42-75) is the actual logic ofcom.google.android.apps.wearables.maestro.companion.devicelogging .KpiLoggingWorker— a genuinely unobfuscated class name (survived R8, imported athvn.java:6) implementing aWorkManager-style worker that fetches raw diagnostic logs directly from the connected earbud (ftjVar.A(b)/ftjVar.g(b),b=device address), gated by the samegcl.B()consent flag already documented elsewhere in this entry — logging"No consent for diagnostics"(hvn.java:67/72) when consent/OOBE-completion isn't met. This is a real, separate on-device-log-retrieval feature, distinct from (but clearly related to) the KPI-event-streaming pipeline already documented above. Case 4 (hvn.java:92-108) references Google's real, publiccom.google.android.libraries.performance.primes.transmitter .clearcut.ClearcutMetricSnapshotTransmitterclass (imported athvn.java:7) — confirming the app uses Google's Primes performance-monitoring library, and that (in that one unrelated case, not the caseghs.java's call goes through) Primes' own metric-snapshot transport is Clearcut-based. This is a useful nuance, not a contradiction: it suggests "Streamz"-labeled metric groups under Primes can still ultimately transmit via Clearcut in this codebase — but this was not confirmed for the specific case-2 pathghs.javaactually uses, which remains unresolved per point 3 above.
- Bounded-scope stopping point (deliberate, not an oversight): this pass stopped at
iqd(an unimplemented interface) and did not openepf.java's case 14 or attempt to locategwv's correct constructor overload — both would be the natural next steps to fully resolve where this pipeline's bytes actually go, but doing so risked an open-ended trace through more R8-merged dispatcher classes without a clear stopping point, which this task's own guardrails asked to avoid. Conclusion stands as a genuine, not-artificially-narrowed negative result for the segment actually traced: no consent/feature-flag gating exists inghs→hvn(case 2) themselves; whether gating exists further downstream, inside the not-yet-locatediqdimplementation, remains an open question, stated as such rather than assumed either way. - Update (2026-08-30, follow-up pass) —
epf.javacase 14 opened; last pass's leadinggwv-constructor lead checked and refuted; the correct constructor narrowed to 3 candidates, not fully resolved; the "no gating" finding holds one level further. 🟢 FACT (code existence/structure only; describes the official app, same caveat as throughout this entry):defpackage/epf.java(final /* synthetic */ class epf implements Callable, 179 lines, read in full) is itself confirmed as another R8-merged multi-case dispatcher (20 cases, same pattern ashvn/fwy/gau/frg/fsz). Case 14 (epf.java:133-134, rendered ascase UrlRequest.Status.READING_RESPONSE /* 14 */:) is a single line:return (iqd) this.a.a();— wherethis.ais whatever object was passed asepf's own constructor argument (ghs.java:203'snew epf(obj2, 14),obj2 = gwvVar.a). No gating check of any kind appears in this case either — extends last pass's "no gating inghs→hvn" finding one level further, toghs→hvn→epf(case 14) as a whole. The concreteiqdimplementation is still one level further away: whatevergwvVar.a.a()(a generic.a()accessor call) returns.- The prior pass's leading candidate — the
gwv(Context, byte[], byte[])constructor atgwv.java:1625-1630(the one immediately adjacent to the"STREAMZ_GNP_ANDROID"-taggedhvrconstruction) — is refuted for this specific chain: it setsthis.a = new CopyOnWriteArrayList();(gwv.java:1627), andCopyOnWriteArrayListhas no.a()method, soepfcase 14'sthis.a.a()could not type-check against it. This doesn't mean that constructor overload is unused elsewhere in the app (it may genuinely back some other, unrelatedgwvusage) — only that it cannot be the oneght'sgwvVarfield resolves to, given whatepf's case 14 requires ofgwvVar.a. - All ~40 of
gwv's constructor overloads were read this pass (gwv.java:1299-1630, plus the earlier-readgwv(List)atgwv.java:52-...) specifically checking each one'sthis.aassignment against the "must have a.a()method" requirement.defpackage/nmx.java(a bare single-method interface,Object a();) is the type this project's own traces have independently, repeatedly confirmed follows exactly this lazy-provider.a()accessor pattern (e.g. thenph.b(...).a()idiom used throughoutfyo/fyp/futand elsewhere) — the only constructors whosethis.ais assigned annmx-typed argument are:gwv(String str, mbr mbrVar, nmx nmxVar)(gwv.java:1359-1363,this.a = nmxVar),gwv(nmx nmxVar, nmx nmxVar2, nmx nmxVar3)(gwv.java:1468-1475,this.a = nmxVar), andgwv(nmx nmxVar, nmx nmxVar2, nmx nmxVar3, byte[] bArr)(gwv.java:1593-1600,this.a = nmxVar3) — these 3 are the strongest remaining candidates, though not confirmed as the one in play; a handful of other constructors assignthis.ato an application-specific type (ipc,jnq,jfl,jtt,bzq,oql, etc.) whose own.a()-method-having-ness was not individually checked this pass, so they are not fully ruled out either, only judged less likely given no independent evidence (unlikenmx) that they follow this accessor convention. - What would be needed to fully resolve this (not done this pass, next natural step): trace
ght's owngwv gwvVarconstructor argument (fuu.java:79's(gwv) this.j.a()) back tofuu's own construction site, to find whichnpois bound tofuu'sjfield — the same method already used successfully for thegbn-singleton trace in an earlier pass. This pass did not attempt it (stopping here per this task's own explicit "close to the final pass on this side-thread" scoping) — recorded as the concrete, actionable next step rather than left vague. - Gating conclusion (updated): extends cleanly — no gating found in
ghs→hvn(case 2)→epf(case 14); whateveriqd's real implementation is (reached via one of the 3nmxcandidates above, or an unconfirmed alternative) remains untraced, so gating still cannot be ruled out or confirmed beyond this point. This is stated as the final open question for this side-thread, not chased further, per this task's own scoping.
- Same class hierarchy, same sink method, not overridden:
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/goq.java:7 - Readable alias: MaestroRouteTarget
- Role: 🟢 FACT: a protobuf-lite-style enum (
goq.java:8-22, values 0–13 plusUNRECOGNIZED(-1)) enumerating pw_rpc routing targets:UNKNOWN,HOST,CASE,LEFT_BT_CORE,RIGHT_BT_CORE,LEFT_SENSOR_HUB,RIGHT_SENSOR_HUB,LEFT_SPI_BRIDGE,RIGHT_SPI_BRIDGE,DEBUG_APP,MAESTRO_A,LEFT_TAHITI,RIGHT_TAHITI,MAESTRO_B. Used as the source/target arguments tofxm'sGetSoftwareInfocalls (see above). - Relevant methods:
b(int):30+— int-to-enum mapping (standard protobuf-liteforNumber-style method).
- Open questions: what
CASE,LEFT_TAHITI/RIGHT_TAHITI,LEFT_SENSOR_HUB/RIGHT_SENSOR_HUB, andLEFT_SPI_BRIDGE/RIGHT_SPI_BRIDGEactually correspond to in hardware. ⚪ ASSUMPTION:CASEis the charging case being independently addressable over this same routing scheme, andTAHITIreads as a chip/SoC codename — neither is capture-correlated or otherwise confirmed.
Other maestro_pw.Maestro/Dosimeter/EartipFitTest/HeadGesture/Multipoint/JitterBuffer request/response types — decoded shapes register
The remaining request/response types named in the fux service-catalog table above, decoded with
scripts/decode_rawmessageinfo.py but not individually traced beyond their raw shape this pass (🟢
FACT for shape/existence; no semantic claim beyond what's noted). nia/qii/qih/qif/qip/qio/
qim are all the same already-documented trivial 0-field marker shape (nia.java:9-style) and are
omitted from the table below (see the existing nia note near this doc's top and the
MaestroEmptyMarker entry above).
| Type | RPC role | Fields | Shape |
|---|---|---|---|
qiv |
GetHardwareInfo response |
6, sparse 1–7 | fields 1,2=ENUM/UINT32(plain); a 2-alt oneof at fields 4/7 (→qjm/qjr); fields 5,6=plain ENUM |
qiy |
SubscribeRuntimeInfo response |
4, sparse 4–7 | 2 separate oneofs: oneof A (fields 4/6 → qjh/qhv), oneof B (fields 5/7 → qji/qhy) |
qiz |
SetWallclock request |
1 | plain UINT64 (Java field c) — a timestamp |
qix |
ReadSetting request |
5 | single oneof, fields 1–5, all ENUM except field 5 (MESSAGE→qjv) — plausibly a "which setting-group, and within group-5 which sub-setting" selector, mirroring qjc/qja's 5-group split; not confirmed |
qje |
SubscribeToOobeActions response |
1 | plain ENUM (Java field c) |
qig |
EartipFitTest.SubscribeToResults response |
2 | 2× plain FLOAT (Java fields c,d) — plausibly a per-ear fit-test score pair |
qin |
HeadGesture.SubscribeToResults response |
1 | plain ENUM (Java field c) — plausibly the detected gesture type |
qhz |
Dosimeter.FetchDailySummaries response |
5 | fields 1,3,4=UINT32, field 2=MESSAGE_LIST(→qia, repeated — plausibly one entry per day), field 5=FLOAT |
qir |
Dosimeter.SubscribeToLiveDb response |
1 (field 2 only) | plain FLOAT (Java field b) — plausibly a live dB reading |
qjs |
JitterBuffer.SetJitterBufferSizePreference request |
1 | plain ENUM (Java field c) |
qij |
Multipoint.ForceMultipointSwitch request |
2 | INT32 + BOOL (Java fields c,d) |
qjp |
Multipoint.SubscribeToQuietModeStatus response |
1 | plain BOOL (Java field c) |
Open questions: none of the "plausibly" readings above are anything more than ⚪ ASSUMPTION from field count/type/RPC-name context — none is capture-correlated. Included here only to keep this register complete for whoever picks up the corresponding capture-correlation work next.
Update (2026-09-13, ai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.md Phase 3) — qin's single
ENUM field's own validity range decoded; no Nod/Shake-named constant found anywhere in the
decompiled tree. 🟢 FACT (code existence): qin's field 1 (c) references a shared,
R8-merged enum-validity checker (qgx, implements mys, a single a(int) method dispatching on a
stored instance index — the same infrastructure class validates ~20 unrelated proto enums across
this APK, one case per enum type); the specific instance qin references (qgx.q, index 16)
routes to a.aO(int) → a.aI(int) != 0, and a.aI(int) returns non-zero only for raw input values
0, 1, and 2 (mapping them to 1, 2, 3 respectively; everything else returns 0/invalid). This means
the wire-format enum HeadGesture.SubscribeToResults returns is a genuine, code-confirmed
3-valued enum (raw values {0, 1, 2}) — structurally consistent with exactly one
unset/unknown sentinel plus the two real gesture types (Nod, Shake) this project already expects,
but no name for any of the 3 values survives anywhere in the decompiled tree: an exhaustive
case-insensitive search for "nod"/"shake"/NOD_GESTURE/SHAKE_GESTURE/HeadGestureType-style
literals across the full jadx-output/ tree found zero matches. Which raw value (0, 1, or 2) is
Nod and which is Shake is not recoverable from static analysis — only a capture with an actual,
triggered, camera-confirmed gesture (designed as CAPTURE_BLUETOOTH_HCI_SNOOP.md Group AQ, planned
CAP-055) can resolve this.
- What this does NOT establish: whether
HeadGesture.SubscribeToResults/qinis even the channel this project would observe on the wire for a Nod/Shake action (theStartDetection/EndDetectionunary calls and this server-stream response are all still unconfirmed against any capture) — this pass only sharpens the shape of what to look for once a capture exists.
Per this task's step 5: a header-only sweep (field_count only, no full field decode) was run across
all 807 new naa( constructions found under
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/ (script:
a small header-only variant of scripts/decode_rawmessageinfo.py's decode_info_string, not itself
committed — the logic is identical to that script's header-parsing prefix). This surfaces schemas
richer than anything traced above, whose owning RPC method/service is not yet identified — a
plain grep for new <ClassName>( and <ClassName>.class from other files found no direct callers for
the top 3, meaning they're most likely referenced only indirectly (e.g. via a service-catalog string
table like fux's, not yet located for these). 🔴 OPEN QUESTION for all of these — no relevance
judgment is made here per AGENTS.md §6/ADR-017's boundary, this is a plain listing:
| Class | Field count | Oneof count | Notes |
|---|---|---|---|
nhm |
76 | 0 | richest schema found in the entire sweep; no external new nhm(/nhm.class reference found |
nef |
74 | 0 | same as above — no external reference found |
qaa |
63 | 0 | same as above — no external reference found; q-prefix (like the WriteSetting family) but not reached from fux's service catalog |
ndi |
35 | 0 | — |
mtn |
30 | 1 | has a oneof, like the WriteSetting family shapes |
nca |
30 | 0 | — |
gdw |
28 | 0 | — |
nfh |
27 | 0 | — |
msw |
26 | 1 | has a oneof |
qaj |
24 | 1 | q-prefix, has a oneof |
qbu |
24 | 0 | q-prefix |
qar |
21 | 0 | q-prefix, has 1 map field (only map-typed schema in the top ~20) |
The full ranked list (top 60 by field count) is reproducible via the header-only sweep described
above; not reproduced here in full to keep this document focused — available on request. None of
these were traced to a specific maestro_pw.* (or other) service/method this pass; that would be the
natural next step for whoever picks this up (search for X.class and X.a reference sites the way
qjc/qjb were traced back to fux.java/fxk.java above).
- Path: exhaustive
grep -racrossreverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/(not a sample — the whole decompiled tree). - Readable alias: n/a (a search result, not a class).
- Role: 🟢 FACT: this APK version's decompiled source contains zero references to
BluetoothGattand zero references toBluetoothGattCallback, anywhere. It also contains zero references to the Fast Pair GATT service UUID0xFE2C(grep -rli "fe2c", no hits). A separate check for the two long-standing open GATT handle numbers fromCAP-014/PROTOCOL.md§4.3's0x0c0X/0x0f2Xcluster (grep -ril "0x0c0c|0c0c|0x0f2a|0f2a") found exactly one file,com/google/android/apps/wearables/maestro/companion/R.java, and both hits there (settingslib_neutral_variant96 = 0x7f060c0c,Widget_Material3Expressive_Toolbar_Surface = 0x7f150c0c) are unrelated Android resource-ID integer constants that merely contain the substring0c0c— not a GATT handle in any Fast-Pair-UUID-adjacent context. This is a genuine negative result, not a coincidental near-miss. - Relevant UUIDs found: none (that is the finding).
- Assessment: corroborates, from the companion app's own source rather than from capture-based
reasoning, this project's existing methodology choice (
CAP-004/CAP-014) of using nRF Connect rather than the official app for all GATT-related captures — the companion app itself performs no directBluetoothGattconnection management at all, consistent with GATT access (Fast Pair Account Key exchange, the Battery Service pairing, etc.) being delegated entirely to Google Play Services/ Nearby out-of-process. It also rules out the companion APK's own source as the place wherePROTOCOL.md§4.3's still-open0x0c0X/0x0f2Xhandle↔UUID mapping question could be resolved — that open question remains exactly as open as before, just with one plausible lead (checking the companion app's own source) now closed off. - Open questions: none — this is a completed, reproducible, exhaustive search. The underlying
0x0c0X/0x0f2Xhandle↔UUID mapping question itself (PROTOCOL.md§6) remains unresolved and is unaffected by this entry.
defpackage.ijk / defpackage.ijp / defpackage.ijm / defpackage.iji / defpackage.gsy / TrueWirelessHeadset / HeadsetPiece / FmdRequest / FmdResponse / FmdWorker — GMS Fast Pair client-library boundary (Chimera-brokered AIDL)
(Added 2026-09-08, implementing ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1/Phase 4, maintainer-approved per prompt 0002.)
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/ijk.java(and sibling files below) - Readable alias: GMS Fast Pair client library boundary
- Role: 🟢 FACT (code existence/structure, traced end-to-end): the companion app's own source
for on-screen battery display and Find My Device Terms-of-Service consent, reached via two
genuinely named, unobfuscated AIDL interfaces bound through Google's Chimera dynamic-module
broker. Not GMS reverse-engineering — this is the companion app's own client-side code calling
out to GMS, fully within this project's existing APK-analysis scope (
DECISIONS.mdADR-017); no GMS-side code was decompiled or analyzed. - Relevant methods/classes:
defpackage/ijk.java:42-52(b()) — buildsIntent("com.google.android.gms.nearby.discovery.fastpair.ACTION_BIND_DEVICE_DETAIL"), targetsintent.setClassName("com.google.android.gms", "com.google.android.gms.chimera.GmsBoundBrokerService"), callscontext.bindService(intent, this.c, 1).defpackage/iji.java:20-33(onServiceConnected) — callsiBinder.queryLocalInterface("com.google.android.libraries.bluetooth.fastpair.IFastPairDeviceDetailService"), falls back tonew ijm(iBinder)(a hand-rolled Binder proxy,defpackage/ijm.java:9).ijk.a(String):29-40— rawParcel-marshalled transact call (code 2) passing a device address, unmarshals the reply ascom.google.android.libraries.bluetooth.fastpair.TrueWirelessHeadset.TrueWirelessHeadsetfields (AutoValue_TrueWirelessHeadset.java'stoString(), unobfuscated):leftBud/rightBud/headsetCase(each aHeadsetPiece),lastUpdateElapsedRealtimeMillis,modelId,firstObservationTimestampMillis,mainIconContentUri,isBatteryAllTheTimeDevice(boolean, not yet interpreted).HeadsetPiecefields (AutoValue_HeadsetPiece.java'stoString()):lowLevelThreshold(int),batteryLevel(int),imageUrl(String),charging(boolean),imageContentUri(Uri).ijk.b()also registers aContentObserveroncontent://com.google.android.gms.nearby.fastpair/battery_status_update— GMS notifies the companion app of a battery change viaContentResolver.notifyChange().defpackage/gsy.java:17(this.i = new ijk(context, this)) —ijk's sole construction site.- Find My Device (FMD), same broker, different action:
defpackage/ijp.java:37-45(b()) buildsIntent("com.google.android.gms.nearby.discovery.fastpair.ACTION_BIND_FMD_PROXY"), sameGmsBoundBrokerServicetarget;iji.java'sonServiceConnectedcase 2 queries"com.google.android.libraries.bluetooth.fastpair.fmd.IFastPairFmdProxyService".ijp.a(FmdRequest):24-35sends anFmdRequest(fields viaAutoValue_FmdRequest:address,acceptedTosVersion, ...) and gets anFmdResponseback. com.google.android.apps.wearables.maestro.companion.fmd.FmdWorker(unobfuscatedandroidx.work.ListenableWorker): staticc(Context, String, int)/n(Context, String)(FmdWorker.java:51-66) buildFmdRequests with operation code3("Enqueued accept worker") /4("Enqueued skip worker") — ToS accept/skip only; no otherFmdRequest.d()call site exists anywhere in this APK version (grep -rn "FmdRequest\.d()"→ 2 hits, both accept/skip).- A third, ContentProvider-based channel to the same authority (not a bound service):
defpackage/hlf.java:705andcom/google/android/apps/wearables/maestro/companion/slices/MaestroSliceProvider.java:233both build/querycontent://com.google.android.gms.nearby.fastpair/links?address=<addr>&caller=maestro.
- Open questions:
TrueWirelessHeadset.modelIdas a candidate cross-reference against DLCI 0x04's already-FACT Device Information "Model ID" (da 2d b1) — checked 2026-09-08 (ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 4 item 3) for an already-available value dump anywhere in this project's existing captures/docs; none found (grep -rli "modelId"acrosscaptures/,DESKRESEARCH_FINDINGS.md, andai-sessions/returns no capture/finding file, only this task's own prompt/result files referencing the open question itself) — confirmed this genuinely needs the maintainer's own live-device access (anadb-level dump or a debug log of the GMS-internalTrueWirelessHeadsetobject), not static analysis or existing capture re-analysis; no value can be obtained without it, since this field is never carried on the wire.isBatteryAllTheTimeDevice's actual value/effect — no getter/log/UI reference traced. The actual FMD "ring"/"play sound" trigger for Case/"both" is not constructed anywhere in this APK's decompiled source (seePROTOCOL.md§4.4/§6's Behavior-section update). - Correlation with
PROTOCOL.md: informational context only (§6 Commands & schemas, 2026-09-08 addition) — explicitly not a candidate implementation path for this project's own app (would require GMS,AGENTS.md§1).
MaestroDeviceSettingsProviderService / defpackage.fhk / defpackage.ges — AOSP Bluetooth-Device-Details settings-extension boundary
(Added 2026-09-08, implementing ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1/Phase 3/Phase 4, maintainer-approved per prompt 0002.)
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/apktool-output/AndroidManifest.xml(service declaration);jadx-output/sources/com/google/android/apps/wearables/maestro/companion/settingprovider/service/MaestroDeviceSettingsProviderService.java -
Readable alias: AOSP Settings-app Bluetooth-device-details extension
-
Role: 🟢 FACT (code existence/structure): a second, previously-undocumented UI entry point into the
qhr/WriteSettingpipeline — the system Settings app's own Bluetooth-device-details page, not one of this project's already-documented in-app screens. Not GMS — this is Android's own platform (AOSPSettingsLib) extension framework. -
Relevant methods/classes:
- Manifest:
exported=true,permission=android.permission.BLUETOOTH_PRIVILEGED,intent-filter action="com.google.android.apps.wearables.maestro.companion.services.BIND_SERVICE". - Extends
gpn, implements callback methods annotated@Override // defpackage.fhk—a(DeviceInfo)/cs(DeviceInfo)/ct(DeviceInfo, DeviceSettingState), usingcom.android.settingslib.bluetooth.devicesettings.{DeviceInfo,DeviceSettingState,ActionSwitchPreferenceState}.fhk's real (unobfuscated) name, confirmed via the exhaustivequeryLocalInterfacesweep below, iscom.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener. ct(...)routes case IDs 2102/2103/2104/2113/2115/2116 into the sameftj/fya-interface accessor chain (.f(),.b(), etc.) this document'sqhrregister already names for several fields — which specificqhrfield each case ID maps to was not individually traced this pass (the same forward-trace technique used for Multipoint/Volume EQ above would resolve this — seeTODO.md's new item).defpackage/ges.java— a shared lambda-dispatch class this service also uses, directly referencingTrueWirelessHeadset— i.e. a second, independent consumer of the GMS battery boundary documented in the entry above.- Case 2104 passes
fpm.ENABLED_HEAD_GESTURES/fpm.UNKNOWN— a promising but unconfirmed lead that this specific case is head-gestures-related (qhrfield 29,PROTOCOL.md§4.5.4).
- Manifest:
-
Exhaustive confirmation: every
queryLocalInterface("...")call in the whole decompiled tree was enumerated this pass (31 unique descriptor strings) —IDeviceSettingsListener(this entry) plus the 2 Fast Pair interfaces (entry above) are the only ones with any Bluetooth/Fast-Pair/ settings relevance; the other 28 are generic Android/AndroidX/GMS-common-services/Play-Core plumbing unrelated to this project's scope. -
Update (2026-09-08,
ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 3 item 1) — all 6 case IDs traced to their exact accessor call, resolving 4 to a specificqhrfield number; 2 do not route throughqhrat all. 🟢 FACT (code call-graph tracing,ct(DeviceInfo, DeviceSettingState)read line-by-line,ftj.A(String)confirmed to returnfyc— the same class already used for the Multipoint/Volume EQ forward traces — and every accessor method it calls followed throughfyc's own source tofyb's numbered dispatcher tofya's (fyo's) exactqhr.b = Nwrite). A useful byproduct:ght.p(String,int,int,boolean)/q(...,fpm)(the analytics loggerg()several cases call before writing) resolves its twointarguments throughfms.m(int)(a UI "page" name — all 6 cases pass11="MORE") and, more usefully,fjm.H(int)(a self-describing internalCATEGORY_*settings-taxonomy name) — an independent, code-internal naming source distinct from anything used to identifyqhrfields so far (not a UI string, not a read-side log message, a third kind of self-describing evidence).- Case
2102→qhrfield 2.e().A(str).f(z)→fyc.f(z)→fyb(z,4)→fya.l(z)=fyo.l(z)(fyo.java:169-188) = field 2. Logged category:fjm.H(14)="CATEGORY_OHD"— a second, independent internal-name source (distinct from the wire-derived label) that corroboratesPROTOCOL.md§4.5.5's "In-ear detection" HYPOTHESIS and the pre-existingqjnentry's own "OHD state, plausibly On-Head-Detection" sibling-field note. Promoted 2026-09-08, maintainer sign-off (DECISIONS.mdADR-019 Update): field 2's field-number/category-level identity ("a real, code-recognized On-Head/In-ear-detection-family boolean") is now 🟢 FACT — "On-Head Detection" and "In-ear detection" are closely related, not verbatim-identical UI wording, so the specific label-equivalence claim (unlike fields 11/15's full identity) is not promoted, only the field-number/category-level identity, at the same evidence bar as fields 22/27. - Case
2103→qhrfield 27.e().A(str).b(z)→fyc.b(z)→fyb(z,3)→fya.e(z)=fyo.e(z)(fyo.java:80-100) = field 27 (the already-known category-level-only "case-sound family boolean,"ADR-019Update). This case does not callg().p()/g().q(), so noCATEGORY_*label is available for it. - Case
2104→qhrfield 11, plus a second, non-qhrwrite; a genuine internal-naming tension, not resolved.e().A(str).e(z)→fyc.e(z)→fyb(z,9)→fya.j(z)=fyo.j(z)(fyo.java:146-166) = field 11 — the exact chain already confirmed for Multipoint. Logged category:fjm.H(15)="CATEGORY_MULTIPOINT"— a second, independent internal-name confirmation of field 11 = Multipoint. Tension: this same logging call also passesfpm.ENABLED_HEAD_GESTURES/fpm.UNKNOWN(a different enum,fpm, used as an outcome/event tag rather than the category) — i.e. the category name says "Multipoint" but the outcome tag says "head gestures," for the same call. Not reconciled; flagged as its own open item below, not silently resolved in either direction. Case 2104 also issues a second, structurally unrelated write:e().j(str).al().e(new gox(new hlb(this, str, z, 1), 6)).p()—ftj.j(String)returnsoql(a generic RxJava-style stream type, notfyc), andhlb's case-1 branch resolves toMaestroDeviceSettingsProviderService.h()(thekjjfield, named"premiumAudioHelper"in its own accessor-exception string) callingkjj.d(String, gcl, boolean), logged"Not required to turn off Feature A"when the condition isn't met. This is not part of theqhr/WriteSettingpipeline at all — a structurally separate mechanism. 🟡 Plausible, unconfirmed reading: enabling Multipoint here also disables an internally code-named"Feature A"as a side effect —fpm.ENABLED_SPATIAL_AUDIOis a plausible (not evidenced) candidate for what "Feature A" is, since Multipoint and head-tracked Spatial Audio are mutually exclusive on many earbuds — offered as a lead for a future pass, not asserted. - Case
2113→qhrfield 5.e().A(str).c(z)→fyc.c(z)→fyb(z,12)→fya.f(z)=fyo.f(z)(fyo.java:102-122) = field 5 (previously "not independently named" in this entry's own bonus register). Nog().p()/g().q()call in this case's path, so noCATEGORY_*label — field 5's semantic identity is still unnamed; only its case-ID association is new. - Case
2115→ does not touchqhrat all. UsesMaestroDeviceSettingsProviderService.h()(kjj,"premiumAudioHelper") directly:kjj.c(String, boolean), logged"calling setFeatureAState"— the same"Feature A"mechanism case 2104 disables as a side effect above. Plausibly this case ID is the system-Settings page's own standalone toggle for whatever "Feature A" is (see the Spatial Audio lead above) — not confirmed, noqhrfield involved. - Case
2116→qhrfield 32 (previously entirely absent from this entry's register, not even as "not found").e().A(str).i(new fyb(z, 6))→fyc.i(pkg)(the generic dispatcher everyfycconvenience method routes through) →fyb(z,6)→fya.r(z)=fyo.r(z)(fyo.java:257-276) = field 32. Logged category:fjm.H(24)="CATEGORY_RV_BLOCK_AUTO_TEST"— an internal diagnostic/automated-test-sounding name, not a recognizable user-facing feature; field 32 is at best a diagnostic-toggle candidate, genuinely unclear, not chased further. - Bonus, beyond this item's own ask — two corrections to this document's
qhrfield register, found while readingfyo.javain full to resolve the cases above (see that entry's own update for the authoritative record; noted here too since it was this trace that found it): field 6 has a real write site (fyo.java:191-210, methodm) — the register's prior "not found (no write site)"/"write-silent" characterization was wrong; its own caller was not found this pass (checkedfyb's cases 0–18 and the only otherfya-referencing dispatcher,hqy.java, and neither reaches.m()) — genuinely still open. Field 32 (fyo.r, above) was entirely absent from the register before this pass.
- Case
-
Open questions: field 5's and field 32's semantic identity (no self-describing name found for either); case 2104's
CATEGORY_MULTIPOINT-vs-fpm.ENABLED_HEAD_GESTURESnaming tension; what"Feature A"/kjj/"premiumAudioHelper" actually is (cases 2104's side effect, 2115's direct toggle);fyo.m()(field 6)'s own caller. -
Hypothesis test: none required for the case-ID→accessor traces above (🟢 FACT, static call-graph tracing with no ambiguous branch); the remaining open questions are further static-analysis tasks (field 5/32/6/"Feature A" naming) or a capture task (wire-confirming field 32 and the field-2/
CATEGORY_OHDstrengthening against real traffic) — not attempted this pass. -
Correlation with
PROTOCOL.md: §6 Commands & schemas, 2026-09-08 addition (original entry); 2026-09-08 update (this trace) proposed forPROTOCOL.md§6/§4.5.5, pending maintainer review — seeai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md.
defpackage.frb / defpackage.fuh / defpackage.glk / defpackage.gjv — fxm.i()/GetSoftwareInfo trigger structure
(Added 2026-09-08, implementing ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 2/Phase 4, maintainer-approved per prompt 0002.)
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/sources/defpackage/{frb,fuh,glk,gjv}.java -
Readable alias:
GetSoftwareInfofetch-trigger dispatcher -
Role: 🟡 HYPOTHESIS (call-graph traced further, a genuine advance over the prior "plausibly connect-adjacent" reading, but not itself confirmed against a capture):
fxm.i()'s trigger for its 4×GetSoftwareInfocalls is plural, not singular. -
Relevant methods:
frb— confirmed a 21-case (0–20) R8-mergedoru/Consumer-shaped lambda dispatcher (same pattern asgau/fwy/hvn/epf); case 13's"Change primary route to %d"log is one specific lambda instance, notfrb's "own" behavior.fxm's constructor (fxm.java:23-44) wiresfrb(13)as a subscriber onfxm.f(an RxJava-style hot-publisher field) — the log fires whenever something else pushes a newgoqvalue onto that field, not on a fixed schedule or directly on connect.fuh— a two-method start/stop-shaped lifecycle interface (void b(); void i();); 6 classes implement it:fxm,fvp(Dosimeter),fwc,fwx(RuntimeInfo/low-latency-mode),fxg,fyq,geg.glk.j():530-534—if (optional.isPresent() && this.g.F() && this.v.q()) { fuh.i() }, inside a class whose surrounding code (glk.java:220-260,500-530) is unambiguously firmware OTA bundle-transfer orchestration (log strings"Device is ready to transfer","Route %d transfer completed","Update bundles not staged","Transfer stopped after transfer failed"). This reframesfxm.i()'s trigger, for this call site, as OTA-transfer-readiness- gated, not a generic post-connect settling action.gjv.p():743-749(implementing interfacegiz'sp()) — gated onhwy.am(this.u)before calling.i();gjvis a large per-device connection/settings-lifecycle class (othergizmethods:m()Auto-OTA check,n(gdm)"wait for device type update to check firmware version",o()calls intogcl,q()). Looks more plausibly connect/pairing-lifecycle-shaped thanglk's OTA-specific call site, but was not traced to its own ultimate caller this pass.
-
Open questions: whether
gjv.p()'s own caller fires inside theCAP-036/CAP-041connect-time settling window — needs either a full trace ofgjv.p()'s caller, or a byte-level correlation between a fresh capture's connect-time burst content and aGetSoftwareInfo-response-shaped payload (qjb, already decoded). -
Hypothesis test: byte-level content diff of a fresh capture's DLCI 0x02 connect-time burst against
qjb's decoded shape, or a full static trace ofgjv.p()'s caller. -
Update (2026-09-08,
ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 3 item 3) — re-attempted;gjv.p()'s own caller was not found this pass either, same conclusion as the prior session, with one adjacent piece of context gained.gizis confirmed an abstract class (not an interface, correcting this entry's earlier looser phrasing), andgjvis its sole subclass in this APK version (grep -rl "extends giz"finds onlygjv.java) — so any external.p()call on agiz-typed reference necessarily reachesgjv.p().giz's own fieldu(the device-type/variant intgjv.p()gates on viahwy.am(this.u)) is a barepublic int, set directly rather than through a named setter — this rules out finding the caller by searching for a setter method name, and (per this document's own zero-creativity standard) that avenue was not pursued further given".u = "is too generic a token to search productively (20+ unrelated files match it combined with the equally generic".p()"). Three classes hold agiz-typed field (gpk/gpr/hke, all reached viaftj.i(String)— the same device-manager accessor interface already used throughout this document) — none of the three calls.p()anywhere in its own file. Adjacent context found:gaa.java(implements fzy, aGetSoftwareInfo-response-shaped event listener — its methodd(qjb qjbVar), line 920, unpacks theqjbresponse type this document's own entry already decoded) calls((giz) this.h.a()).n((gdm) of.get())at line 1043, inside a block logged"OTA handler handleFirmwareInfoUpdate"— i.e.gaais the RESPONSE-side handler forGetSoftwareInfo, calling a differentgizlifecycle method (n(gdm), "wait for device type update to check firmware version") once a response arrives — the reverse direction fromgjv.p(), which triggers the request. This confirmsgaa/giz.n()andgjv.p()/fuh.i()are two ends of the same round-trip, but does not itself locate.p()'s trigger. Recommendation, unchanged from the prior session: the byte-level capture-correlation path (againstqjb's decoded shape) is now the more promising route — pursued this same session, seePROTOCOL.md§6'sCAP-041/CAP-036connect-time-burst item andai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 4 item 1. -
Correlation with
PROTOCOL.md: §6 Commands & schemas' existing open item on the connect-time RPC burst (CAP-036-FINDINGS.md§4), updated 2026-09-08 with this deepened (not fully resolved) trace. -
Update (2026-09-13,
ai-sessions/0013_FEATURE_RESULT_2026_09_13.mdPhase 8) —gjv.p()'s own caller found, via a materially different search strategy from the two prior (failed) passes: a direct smali method-descriptor search rather than a generic Java-source token grep. Both prior passes searched decompiled Java source for generic tokens (.p(),.u =) and were swamped by false positives; this pass instead searchedapktool-output/'s smali for the literal invoke targetLgiz;->p()(the abstract methodgjv.p()overrides) — a search space too specific for R8 obfuscation to genericize away. Result: exactly one match,apktool-output/smali_classes2/ftw.smali:337(invoke-virtual {p0}, Lgiz;->p()V), insidepswitch_8offtw's single dispatch methoda()V—ftw(implements Lorr;) is the same R8-merged,int-tagged synthetic-lambda-dispatcher shape already documented elsewhere in this file forfrb/gau/fwy/hvn/epf(apacked-switchon a storedintfield selects one of 18 cases, 0x0–0x11).pswitch_8corresponds to case index9(packed-switch 0x0table, reverse-listed: index 0→pswitch_11… index 9→pswitch_8).- Of
ftw's 19 construction sites across 15 files (grep -rl "Lftw;-><init>"), exactly one passes case index9(const/16 v0, 0x9):apktool-output/smali_classes2/gag.smali:158(insidegag's ownpswitch_4block, itself another R8-merged dispatcher of the same shape,implements Lorz;, resembling a GuavaAsyncFunction/ListenableFuture-transform callback —Loqh;->f(Lorr;)Loqh;immediately follows theftwconstruction, consistent withcom.google.common.util.concurrent.Futures.transformAsync-shaped code). gag'spswitch_4block first doescheck-cast p1, Ljava/lang/Boolean;on its input, and JADX's partial decompile of the surrounding method (gag.a(Object), undecompilable in full — "Method dump skipped, instructions count: 620") surfaces one readable fragment via its own "Code restructure failed" warning comment:if (Instant.now().minusMillis(((Long) r7.get()).longValue()).toEpochMilli() >= 604800000) goto L35;—604800000ms is exactly 7 days, structurally shaped like a staleness/last-checked-timestamp gate ("has it been ≥7 days since some cached instant?").- What this does NOT establish: whether this 7-day check is the actual, sole gate on
gjv.p()/fxm.i()'sGetSoftwareInfofetch (as opposed to one of severalBoolean-producing branches ingag's own multi-case dispatcher feeding into this specificpswitch_4outcome), or what schedulesgag's own invocation in the first place (its 16 construction sites were not individually traced this pass — out of proportion to this bounded phase's time-box). This is a materially different, and more plausible, trigger shape than either of this document's own prior candidates (glk's OTA-transfer-readiness gate, or a generic post-connect settling action) — a periodic, ~weekly staleness check, not obviously connect-time-adjacent at all — but it is a proposal for the maintainer's review, perDECISIONS.mdADR-017's boundary, not a resolution: this pass did not confirm the 7-day reading against any wire capture, and does not itself decide whether this changesPROTOCOL.md§6's connect-time-burst framing for this trigger candidate. - Recommendation, given the time-box: a fourth static-analysis pass tracing
gag's own 16 construction sites (to find what schedules this specificBoolean-gated case) is a reasonable next step if this lead is judged worth pursuing further, but was not attempted this session.
- Of
(Added 2026-09-08, implementing ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1/Phase 4, maintainer-approved per prompt 0002.)
-
Path:
reverse-engineering/apk/v1.0.955078536-10253511/apktool-output/AndroidManifest.xml(service declaration,grpc.ondevicegrpcserver) -
Readable alias: on-device gRPC endpoint service
-
Role: 🔴 OPEN QUESTION (existence confirmed 🟢 FACT; purpose unresolved): manifest declares
MaestroEndpointService, exported with noandroid:permissiongate, hosting a generic on-device gRPC server (mig/oez— a method-descriptor-keyed dispatch table structurally likeio.grpc.ServerServiceDefinition). -
Relevant methods:
onCreate()is JADX-undecompilable bytecode ("Method dump skipped... 599 instructions") — read via theapktoolsmali fallback this pass. -
Update (2026-09-08,
ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.mdPhase 3 item 2) —onCreate()read viaapktool's smali output; the service registration mechanism and its authorization model are now understood, though the literal service names are not. 🟢 FACT (smali read,apktool-output/smali_classes2/.../MaestroEndpointService.smali:123onward, 1376-line file, perAPK_REVERSE_ENGINEERING_PROCEDURE.md§6/DECISIONS.mdADR-017's smali-explanation boundary):onCreate()reads the injected fieldMaestroEndpointService.b(declaredpublic Map b;in the JADX source), cast tolov(a Guava-style immutable-map type), and iterates its entry set — each entry isMap.Entry<String, Optional<ofd>>. For each entry, if theOptionalis present it's added to a localHashMapand logged"Service %s included"(%s= the entry's String key); if empty, logged"Service %s is not included"— i.e. the registered-service list is a Dagger/Hilt multibinding assembled elsewhere in the app's dependency graph (aMap<String, Optional<ofd>>), not a literal list inside this class. No Dagger-generated multibinding-provider class survives with a findable name in this app version (checked;grep -rl "MembersInjector"finds nothing for this class), so the literal service-name strings (the Map's keys) were not recovered this pass — a genuinely open question, not a gap in the smali read itself.ofdis an authorization-policy interface, not a service-dispatch interface — its single abstract method,oei a(int i), takes a calling UID (not a method index, despite the interface's shape resembling a gRPC method dispatcher at first read) and returns an allow/reject decision (oei.b= allow;oei.f.e("<reason>")= reject with a reason string). Two concrete implementations exist in this APK version:ofb(ofb.java): allows only if the caller's UID equals a specific constant (i == ofc.a) — otherwise rejects with"Rejected by (internal-only) security policy". This isofi's own default policy (ofi.java's constructor:this.e = new ofb();).mie(mie.java): checks the caller's UID's package name against an allowlist (lpl, a GuavaImmutableSet) and verifies that package is signed by a specific signer (hyp, a signature-verification helper) — rejects with"Rejected by (1st-party only Allowlist) security policy. Not google-signed."or"...Package not allowed."if either check fails. The one call site found this pass constructsmiewith a dynamically resolved single package name (new lsk(((gri) obj).b())— a GuavaImmutableSet.of(...)of one runtime-resolved string, not a hardcoded literal allowlist), for an unrelated, incidentally-discovered outbound gRPC client connection (see below) — so this specific call site does not itself confirm which package(s)mieallowlists forMaestroEndpointService's own inbound registrations.oex(abstract,@Deprecated,a(int)throws) — a legacy/unused base class, not chased further.
- This directly answers, in substance, the "is anything (GMS included) actually gated to bind to
this" question this project's own open item asked — not by naming GMS specifically, but by
showing the exported-with-no-
android:permissionmanifest declaration is not the whole picture: the service applies its own per-registered-service, application-layer authorization (either same-app-UID-only, or an allowlisted-and-Google-signed-caller check) before honoring a call. Whether GMS specifically is ever in the "1st-party" allowlist for any of this service's own registered gRPC methods was not determined — the concrete allowlist contents forMaestroEndpointServiceitself were not found (see above). - Incidental, unrelated finding: the
mie/ofb/ofd/ofitypes are a generic, reusable on-device-gRPC authorization framework, not specific toMaestroEndpointService. The one concretemieconstruction site found (fsg.java, a large R8-merged factory dispatcher, case 1) is building an outbound gRPC client channel from this companion app to a separate Pixel system app:new ComponentName("com.google.android.apps.pixel.dcservice", "com.google.android.apps.pixel.dcservice.sdk.service.ExportedEndpointService"), with an inline comment string"Don't use RemoteEndpoints-created channels to access in-app gRPC services". This is a genuinely new, unrelated finding (this companion app is also a gRPC client of a different Pixel-specific system service,dcservice— plausibly "Device Configuration/Personalization Service"), out of scope for this project's Bluetooth/libmaestrofocus — noted here since it surfaced incidentally while tracingmie's only construction site, not pursued further.
-
Open questions: the literal gRPC service name(s) registered on
MaestroEndpointService(the Dagger multibinding's own assembly site was not found); which specific authorization policy (ofb/mie/neither, i.e.Optional.empty()) applies to each registered service; whether GMS specifically is ever in amie-style allowlist for this service. Plausibly unrelated to DLCI 0x04/0x08 entirely (could be a different feature, e.g. cross-device sync) — this pass found nothing connecting it to Bluetooth/libmaestroat all, strengthening that reading. -
Hypothesis test: none further for the mechanism itself (🟢 FACT, smali-read); finding the literal service names would need locating the Dagger/Hilt multibinding-assembly code for this specific
Map<String, Optional<ofd>>— not attempted further this pass (a search across every R8-merged factory dispatcher in the app was judged out of proportion to this item's scope). -
Correlation with
PROTOCOL.md: §6 Commands & schemas, 2026-09-08 addition (original entry); 2026-09-08 update (this trace) proposed forPROTOCOL.md§6, pending maintainer review — seeai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md.
Template per class — copy for each new finding. Every finding must cite the exact decompiled file and line number (e.g.
reverse-engineering/apk/v1.2.3-45/jadx-output/sources/com/google/.../Xy2.java:142), not only a class name — this is whatPROJECT_RULES.md§1 rule 3 already requires of aREVERSE_ENGINEERING.mdevidence source; the Path field below must always include:line_number, never a bare file path.
- Path:
reverse-engineering/apk/v<versionName>-<versionCode>/jadx-output/.../ClassName.java:<line_number> - Readable alias: (e.g. "GattCallbackImpl")
- Role: (e.g. "🟢 FACT: implements
BluetoothGattCallback, receivesonCharacteristicChangedfor battery updates" or "🟡 HYPOTHESIS: appears to serializeAncCommandbased on field names, not yet confirmed against a capture") - Relevant UUIDs found: (list, cross-reference §UUID register)
- Relevant message groups/codes found (if Fast Pair Message Stream-related,
cross-reference
PROTOCOL.md§2.1): (list) - Relevant methods:
methodName(...):<line_number>— (what this method does, and why you think so)
- Hypothesis test (required whenever Role is marked 🟡 HYPOTHESIS —
omit only for 🟢 FACT/⚪ ASSUMPTION/🔴 OPEN QUESTION entries): which
action/Test-ID (
TESTPLAN_BLUETOOTH_HCI_SNOOP.md) would need to be captured to confirm or refute this reading against real wire traffic — mirrorsPROJECT_RULES.md§4's hypothesis-test discipline already used for captures, so code-derived and wire-derived evidence stay linked instead of building two separate, unlinked evidence trails. - Open questions: (what is still unclear)
All UUIDs found in the APK, with status. Once a UUID's function is confirmed
via a capture, update its status here and promote it into PROTOCOL.md.
| UUID | Found in (file:line) | Suspected function | Status |
|---|---|---|---|
3a046f6d-24d2-7655-6534-0d7ecb759709 (byte-reversed alias: 099775cb-7e0d-3465-5576-d2246d6f043a) |
fzd.java:9, gbm.java:38 |
App's own log label: "default internal rfcomm socket" | 🟡 HYPOTHESIS — not yet matched against a capture's SDP record |
25e97ff7-24ce-4c4c-8951-f764a708f7b5 (byte-reversed alias: b5f708a7-64f7-5189-4c4c-ce24f77fe925) |
fzd.java:9, gbm.java:35 |
App's own log label: "pigweed internal rfcomm socket" — SDP-confirmed (CAP-001/CAP-002/CAP-032) as RFCOMM server channel 1 = DLCI 0x02, AGENTS.md §6's Pigweed pw_hdlc channel |
🟢 FACT for channel ownership (confirmed by capture IDs CAP-001/CAP-002/CAP-032, DECISIONS.md ADR-018, PROTOCOL.md §2.2a); 🟡 HYPOTHESIS (strong) that Sent-direction payload content specifically carries libmaestro's settings commands |
00001124-0000-1000-8000-00805f9b34fb |
fxm.java:12 |
Bluetooth SIG-assigned HID Profile UUID (public spec, not project-specific) — app checks for it before triggering fetchUuidsWithSdp() |
🟢 FACT (that this official UUID is checked for); whether the Buds actually expose it is capture-dependent — cross-reference CAP-002/CAP-016 |
Checked and confirmed absent, 2026-09-09 (ai-sessions/0004_MAINTENANCE_RESULT_2026_09_09.md
Task 2) — a clean negative, recorded so a future pass doesn't re-attempt the same search assuming it
was never tried. CAP-034 (2026-09-01) independently resolved a full 15-service GATT UUID mapping
via wire capture alone (PROTOCOL.md §6, §4.3 Option D): the Fast Pair Service (0xFE2C) and its
characteristics (FE2C1233–FE2C1239), Device Information (0x180A), Battery Service
(0x180F)/Battery Level (0x2A19), Firmware Revision String (0x2A26), Accessory Non-Owner Service
(15190001-12f4-c226-88ed-2ac5579f2a85), and the still-unnamed "Unknown Service"
(109b862f-50e3-45cc-8ea1-ac62de4846d1). A case-insensitive search of the entire decompiled tree —
jadx-output/sources/ in full (both exact-case and grep -li) and apktool-output/smali*/ — for
every one of these UUIDs, in both their full 128-bit and short 16/32-bit forms, found zero genuine
matches: the 5 smali hits that did surface (akm.smali, hlf.smali, dps.smali, pex.smali,
TestingToolsBroadcastReceiver.smali) are all coincidental hex substrings inside unrelated numeric
constants (a double literal, a resource ID, a hashCode-shaped constant, a serialVersionUID, a
switch-case hash) — none is an actual UUID reference, individually verified by reading each hit's
surrounding line. This is consistent with, and further corroborates, DECISIONS.md ADR-025's
existing finding: this companion app's own decompiled source contains no trace of DLCI 0x04/0x08's
GATT/Fast-Pair-service handling at all — that layer is implemented entirely inside Google Play
Services, not this APK. No new register rows are added for these 8 UUIDs, per this document's own
scope note (APK findings only) — their wire-level identity is already fully established in
PROTOCOL.md directly from CAP-034's own capture evidence, which does not need (and does not get)
a redundant APK-code citation here.
If the Message Stream framing hypothesis (PROTOCOL.md §2.1) is confirmed,
use this table for vendor-specific Message Group/Code values found in the
APK, in addition to the officially documented ones.
| Group | Code | Found in (file:line) | Suspected function | Status |
|---|---|---|---|---|
Empty by design, not by omission (noted 2026-09-07, AUDIT_REPORT_2026-09-07.md §1.0/§2.1,
DECISIONS.md ADR-025): exhaustive full-tree searches found no code anywhere in this companion
app's own decompiled source constructing or parsing a DLCI 0x04 Fast Pair Message Stream frame or
DLCI 0x08's private envelope (no MessageStream/HearableControls/ANC-opcode literals, no
"GSND"/capability-string matches). Both channels' transport appears to be implemented entirely
inside Google Play Services, which is out of scope for this project's reverse-engineering effort
(DECISIONS.md ADR-025) — this table is expected to stay empty for DLCI 0x04/0x08 specifically; all
of this project's actual Group/Code knowledge for these two channels comes from wire captures
(PROTOCOL.md §4.1/§6), not from this document.
"GSND" naming lead (DLCI 0x08 "GSND CONTROL"/DLCI 0x0a "GSND AUDIO") — re-searched 2026-09-13, a related string family found, not the literal abbreviation itself
(Added 2026-09-13, ai-sessions/0013_FEATURE_RESULT_2026_09_13.md Phase 8, within DECISIONS.md
ADR-017's mechanical-assistance boundary — search/list/explain only, not a relevance or promotion
decision.)
- Path:
reverse-engineering/apk/v1.0.955078536-10253511/jadx-output/resources/assets/1/tokenized_logs,.../jadx-output/resources/assets/2/crash(both CSV-formatted tokenized log/crash-string tables bundled as APK assets, not.javasource — no single line number applies; matches are scattered throughout each file). - Commands run (repeating and extending
CAP-033-FINDINGS.md§3's original 2026-08-30grep -ri "gsnd"sweep ofjadx-output/, per this phase's instructions):$ grep -ril "gsnd" jadx-output/ # 0 matches (reconfirms the 2026-08-30 negative) $ grep -ril "gsound" jadx-output/ # 2 matches (new this pass) $ grep -rEil "google[._-]?sound" jadx-output/ # 0 matches $ grep -rn "GSND" jadx-output/ apktool-output/ apktool-output-arm64_v8a/ # 0 matches (case-sensitive) - Finding: the literal string
"GSND"(or"gsnd", any case) still does not appear anywhere injadx-output/or either smali tree — the 2026-08-30 clean negative reconfirms. A related but distinct string,"gsound", appears 43 times across the two tokenized log-string CSV assets above, exclusively as substrings of bundled on-device-firmware source-file paths, e.g.:../../services/ble_profiles//voicepath/gsound/{ams,ancs,bms,gsound}_gatt_server.c,../../services/voicepath/gsound//gsound_target/{gsound_ota.cpp,gsound_presto_device_actions.c, gsound_service_utils.c,gsound_target_os.c}. These are firmware crash-log/telemetry string tables (used to decode on-device crash dumps sent back through the app, not app logic itself) —"gsound"is not a class name, UUID, or opcode; it is a firmware source-directory component name embedded in bundled string data. - What this is, and is not:
"gsound"is a plausible, unconfirmed expansion for the"GSND"abbreviation seen inCAP-033's SDP service names ("GSND CONTROL"/"GSND AUDIO") — the string family (voicepath/gsound/..., GATT-server source files, an OTA module) is at least thematically consistent with a BLE-based audio/voice-control accessory service, which is what DLCI 0x08/0x0a's own SDP entries describe. This is not a confirmed identification — no direct textual link between the literal 4-letter "GSND" and "gsound" was found (no comment, constant, or string concatenation anywhere ties the two together), and this class of asset (a firmware crash-string table) is bundled for crash-dump decoding, not proof that the companion app itself implements or names anything "GSND". Proposed as a new, more specific search lead for a future pass — not proposed as aPROTOCOL.md§2.3/§6 update at this time, perAGENTS.md§6/§15 (an AI session may propose, never commit, a finding of this kind). - Hypothesis test: none proposed this pass beyond further static search — a wire-level test
isn't applicable to a naming-etymology question. A future pass could search for other file-path
fragments from the same string family (
voicepath,ble_profiles) to see whether they surface any additional, more specific naming correlation. - Correlation with
PROTOCOL.md: §2.3's 2026-08-30 update / §6's DLCI-0x08-ownership open item — this is a proposed addendum (a related-but-unconfirmed string family, not a resolution), pending maintainer review before either section is edited.
| File | Architecture | Suspected function | Analyzed? |
|---|---|---|---|
libandroidx.graphics.path.so |
arm64-v8a (present only in split_config.arm64_v8a.apk, absent from base.apk) |
AndroidX Graphics Path native backend (System.loadLibrary("androidx.graphics.path")) — unrelated to Bluetooth/libmaestro |
Done — ruled out as protocol-relevant |
libpw_tokenizer_jni.so |
arm64-v8a (same split as above) |
Pigweed pw_tokenizer JNI bridge (System.loadLibrary("pw_tokenizer_jni")) — log-string detokenization support, not the protocol transport itself |
Done — ruled out as protocol-relevant |
No file named libmaestro.so/libgfps.so exists anywhere across base.apk or either split, and no
System.loadLibrary/System.load call in the decompiled sources names one either (full-tree grep,
see the "APK metadata" table above and AGENTS.md §0's 2026-08-30 correction) — the Maestro control
logic is pure Kotlin/Java, not a native binary, for this analyzed version.
Updated 2026-08-30 (
DECISIONS.mdADR-017, superseding ADR-003): native.sodisassembly is now in scope for AI mechanical assistance, on the same terms as DEX/Java-level work — an AI session may runpbtk-from-binaryor disassembler tooling (Ghidra/radare2), search its output, and explain the syntax/structure of already-surfaced disassembly. An AI session still does not decide which disassembled function/struct is relevant, and does not decide that something becomes a recorded finding here or inPROTOCOL.md— both remain the maintainer's calls (AGENTS.md§6/§15, unaffected by ADR-017). This table tracks what exists and what's been analyzed, not a request for the AI to unilaterally decide what it means.
Build this up incrementally — start at the BLE/RFCOMM connect entry point and work outward.
ClassicBTReceiver (AndroidManifest.xml:79, exported=true)
listens for: android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED,
android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED,
android.bluetooth.device.action.ACL_CONNECTED / ACL_DISCONNECTED
-> ??? (not traced this pass — next entry point to follow)
gbm.a() [InternalRfcommSocketSelector]
-> fzd.a / fzd.b [InternalRfcommUuidRegistry — UUID list + normalization map]
-> gau.a() case 3 [RfcommUuidNormalizer]
-> gbd (via b or b2) [InternalRfcommConnection — wraps BluetoothSocket]
fxm.i() [MaestroSoftwareInfoAndHidUuidCheck]
-> fxm.c(goq, goq) x4 [maestro_pw.Maestro/GetSoftwareInfo pw_rpc unary,
one per {MAESTRO_A,MAESTRO_B} x {LEFT_BT_CORE,RIGHT_BT_CORE}]
-> BluetoothDevice.fetchUuidsWithSdp() [only if HID UUID 0x1124 not yet present]
fye.a(qhs) / fsz's WriteSetting send path [confirmed end-to-end, see the nqx/npy/nqo/npw/nqm
entry's "Full confirmed send chain" above]
-> nqo.e(qjc) [pw_rpc.MethodClient.invoke]
-> npy.a(...) [pw_rpc.Client — builds/serializes the RpcPacket, payload=serialized qjc]
-> npw.a(bytes) [pw_rpc.Channel]
-> npv.a(bytes) [one of frg.java's 6 anonymous ChannelOutput implementations]
-> fut.f(bytes, goq) [HDLC-encode: flag + LEB128 address + control + payload + CRC-32 + flag]
-> ffd.j() = BluetoothSocket.getOutputStream()
Not yet traced: how ClassicBTReceiver's connection-state events lead into gbm's socket
selection — a plausible next step for a follow-up §4 pass, not claimed here. (The other half of
this section's original open item — how fsz's WriteSetting/fux's per-service pw_rpc calls
obtain their MethodClient — was resolved by a later pass the same day; see the second call-graph
block above and the nqx/npy/nqo/npw/nqm entry for the full trace.)
Track which findings here have been cross-checked against a capture and promoted into the protocol documentation, to avoid the same finding being "rediscovered" independently in both documents.
| Finding (this doc) | Promoted to PROTOCOL.md section |
Date | Capture/Finding ID |
|---|---|---|---|
gbm/fzd — "pigweed internal rfcomm socket" UUID (25e97ff7-...) = RFCOMM channel 1 = DLCI 0x02 (channel ownership only, not Sent-payload content) |
§2.2a "Channel ownership", §2.3 three-channel table, 2026-08-14 addendum Status line, §4.2 EQ entry | 2026-08-30 | CAP-001, CAP-002, CAP-032; DECISIONS.md ADR-018 |
qhr field 4 = "Use touch controls" master enable toggle (write site fyo.java:124-144, read site fxb.java case 4) |
§4.5.3 top-level toggle opcode | 2026-08-30 | CAP-020 frame 1741; DECISIONS.md ADR-019 |
qhr field 7 = qju — Left/Right press-and-hold gesture-action customization (write site fyo.java:300-374, read site fxb.java case 7) |
§4.5.3 press-and-hold action-selection opcode | 2026-08-30 | CAP-021 frames 1895/3619/4315/4976; DECISIONS.md ADR-019 |
qhr field 12 = qht — field-number identity only (write site hgj.java:216-331, read site fxb.java case 12); "ANC gesture loop"/"ANC-mode rotation checklist" equivalence explicitly not promoted |
§4.5.3 ANC-mode rotation checklist opcode | 2026-08-30 | CAP-021 frames 5237/5247/5255; DECISIONS.md ADR-019 |
qhr's oneof structure confirmed inside DLCI 0x02's field5{field4{...}} wrapper, for fields 4 and 29 sampled at the wire level |
§2.2a 2026-08-30 update | 2026-08-30 | CAP-020 frames 1741/1935; DECISIONS.md ADR-019 |
qhr field 17 = "Volume balance" — full identity, plus zigzag/SINT32 decoding correction (write site fxf.java:82-133, read site fxb.java case 17) |
§4.5.7 Volume Balance opcode/payload | 2026-09-03 | CAP-022 frames 1922/1944/2019/2039/2056/2073/2099; DECISIONS.md ADR-019 Update |
qhr field 19 = "Mono audio" — full identity (write site fyo.java:278-298, read site fxb.java case 19) |
§4.5.5a Mono audio (new subsection) | 2026-09-03 | CAP-022 frames 1621/1823; DECISIONS.md ADR-019 Update |
qhr field 22 = "Speech Detection" — field-number/type identity only (write site hnz.java:29-49, read site fxb.java case 22); "Conversation Detection" UI-label equivalence explicitly not promoted |
§4.5.1 Conversation Detection opcode/payload | 2026-09-03 | CAP-019 frame 1808; DECISIONS.md ADR-019 Update |
qhr field 27 = case-sound-family boolean — category-level identity only (write site fyo.java:80-100, read site fxb.java case 27); "Other alerts"/"Other notifications" label explicitly not promoted |
§4.5.8 Case sounds opcode/payload | 2026-09-03 | CAP-024 frames 2053/2084; DECISIONS.md ADR-019 Update |
qhr field 28 = "Bud return"/"Earbuds replaced" — full identity (write site fyo.java:58-78, read site fxb.java case 28) |
§4.5.8 Case sounds opcode/payload | 2026-09-03 | CAP-024 frames 1988/2023; DECISIONS.md ADR-019 Update |
qhr field 11 = "Multipoint" — full identity, forward-traced from MultipointFragment's toggle via hiy.java:32's "Set device Multipoint as: %s" log to write site fyo.java:146-166 |
§4.5.2 Multipoint opcode/payload | 2026-09-08 | CAP-019 frame 2293; ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 2; DECISIONS.md ADR-025 Update |
qhr field 15 = "Volume EQ" — full identity, forward-traced from hlv.java:2127's "Set volume eq: %s" log (gated on literal preference key "volume_eq_switch") to write site fyo.java:376-396 |
§4.5.6 Volume EQ opcode/payload | 2026-09-08 | CAP-022 frames 1871/1895; ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 2; DECISIONS.md ADR-025 Update |
ijk/ijp/TrueWirelessHeadset/FmdWorker — GMS Chimera-brokered Fast Pair client-library boundary (battery display, FMD ToS consent); informational context only, not an implementation path |
§4.3/§6 Commands & schemas (informational note) | 2026-09-08 | ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1 |
MaestroDeviceSettingsProviderService/fhk/ges — second UI entry point into qhr/WriteSetting, via the system Settings app's Bluetooth-device-details page |
§6 Commands & schemas (new open item) | 2026-09-08 | ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1 |
FmdWorker/ijp — companion app's own FMD code constructs only ToS accept/skip requests, no ring/play-sound trigger found anywhere |
§4.4/§6 Behavior (existing open item, updated) | 2026-09-08 | ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 3 |
MaestroEndpointService — exported, no-permission on-device gRPC server, registered service(s)/caller(s) undetermined |
§6 Commands & schemas (new open item) | 2026-09-08 | ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.md Phase 1 |
MaestroDeviceSettingsProviderService's 6 case IDs traced: 2102→qhr field 2 (+CATEGORY_OHD internal-name corroboration, promoted to category-level identity, maintainer sign-off), 2103→field 27, 2104→field 11 (+CATEGORY_MULTIPOINT corroboration, plus a non-qhr "Feature A" side-write and an unreconciled fpm.ENABLED_HEAD_GESTURES naming tension), 2113→field 5, 2115→no qhr field (a standalone "Feature A" toggle), 2116→field 32 (new field, CATEGORY_RV_BLOCK_AUTO_TEST) |
§6 Commands & schemas / §4.5.5 | 2026-09-08 | ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md Phase 3 item 1; DECISIONS.md ADR-019 Update |
qhr field register corrections: field 6 has a real write site (fyo.m, caller unfound) — corrects a prior "not found" entry; field 32 (fyo.r) added, previously absent |
(register correction, no PROTOCOL.md section — informational) |
2026-09-08 | ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md Phase 3 item 1 |
MaestroEndpointService.onCreate() (smali read): registered services come from a Dagger multibinding (names not recovered); ofd's method is a per-call UID-based authorization check, not a service dispatcher — two policies found (ofb internal-UID-only, mie allowlisted+Google-signed). Incidental finding: an unrelated outbound gRPC client connection to com.google.android.apps.pixel.dcservice |
§6 Commands & schemas (updated) | 2026-09-08 | ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md Phase 3 item 2 |
fyd.d/fyd.e's call sites traced: field 16 fires from the slider-drag/preset path (as understood); field 18 is reachable only via a dedicated, self-describing Save-button click handler — contradicts, not confirms, CAP-015's own "fires on slider-release" wire-timing hypothesis |
§4.2 (updated), §6 (updated) | 2026-09-08 | ai-sessions/0003_MAINTENANCE_RESULT_2026_09_08.md Phase 3 item 4 |
- JADX can misdecompile some optimized/obfuscated constructs — when in doubt,
check against the smali output from
apktool. - Reflection-based code can remain hidden from static analysis — dynamic
analysis (Frida) may be needed here; log any such experiment in the
relevant capture's
CAP-NNN-FINDINGS.mdfirst, perPROJECT_RULES.md§4. - Protobuf field names recovered from JADX (via getter/setter naming) are
not proof of the actual wire field numbers — field numbers, not names,
determine binary compatibility, and must be confirmed via
pbtkextraction or capture correlation before being treated as 🟢 FACT. - A class or method being present in the APK does not prove it is actually
exercised by the specific user actions listed in
TESTPLAN_BLUETOOTH_HCI_SNOOP.md— treat static findings as 🟡 HYPOTHESIS until correlated with a capture showing the corresponding traffic. - Low priority, noted 2026-09-07 (
EXTERNAL_REVIEW_VALIDATION_2026-09-07.md): file+line citations in this document are only reproducible against the exact decompiler version that produced them —reverse-engineering/APK_VERSIONS.md's "Tool-version pinning" section already requires recording JADX/apktool/pbtk versions per analyzed APK version for this reason. A further, not-yet- adopted enhancement worth considering: storing a structural signature or short code excerpt alongside a citation, not just the file+line number, so a citation survives a future JADX/apktool version bump even without re-running the exact tool version that produced it.
https://github.com/tedsluis/opencontrolpixelbudspro2/blob/main/REVERSE_ENGINEERING.md - https://tedsluis.github.io/opencontrolpixelbudspro2/REVERSE_ENGINEERING