Purpose: step-by-step procedure to pull, store, decompile, and analyze the official Pixel Buds
companion app APK, in order to fill in the confidence-rated placeholders in REVERSE_ENGINEERING.md
and, once wire-correlated, PROTOCOL.md. This document is the how — REVERSE_ENGINEERING.md is
the what we found, and reverse-engineering/APK_VERSIONS.md is the which APK version(s) we've
looked at.
This procedure follows AGENTS.md §6/DECISIONS.md ADR-017's AI-assistance boundary throughout:
an AI session may run the mechanical steps below (pulling, hashing, decompiling, running pbtk,
keyword/string searching, explaining already-surfaced code or disassembly — including native .so
disassembly output, per ADR-017 §4), but never decides which candidate is relevant to the protocol
and never decides that something becomes a recorded HYPOTHESIS in REVERSE_ENGINEERING.md. Both of
those remain the maintainer's calls at every step marked [Maintainer decision] below.
- JADX, apktool, and pbtk installed (
WORKSTATION_PREPARATIONS.md's "Reverse engineering tools: JADX, apktool, pbtk" section) — pbtk's own dependencies (Python ≥ 3.10, PySide6, python-protobuf, andjad/dex2jarfor some extractor scripts) confirmed there too. Verified 2026-08-30:apktool --version→3.0.3,jadx --version→1.5.1,pipx list→pbtk 1.1.3withpbtk-jar-extract/pbtk-from-binaryonPATH. - Android platform-tools (
adb) installed and on yourPATH(already required byCAPTURE_BLUETOOTH_HCI_SNOOP.md§1.1). Verified 2026-08-30:adb --version→1.0.41/37.0.0-android-tools. -
sha256sum(standard on Fedora). Verified 2026-08-30: GNU coreutils 9.10.
- The official Pixel Buds companion app installed under the maintainer's own Google account,
on the maintainer's own device — this is the provenance basis
PROJECT_RULES.md§8 rule 20 requires; seereverse-engineering/APK_VERSIONS.md's provenance column. Confirmed by maintainer 2026-08-30: Play Store, maintainer's own Google account, installed on the maintainer's own Pixel 7a. - USB debugging enabled (same as
CAPTURE_BLUETOOTH_HCI_SNOOP.md§1.2). Confirmed 2026-08-30:adb devices -lshows the Pixel 7a (38021JEHN07835, modellynx) authorized, andadb pullsucceeded against it.
-
AGENTS.md§4/§6 (proto-schema extraction rules, sign-off requirement). -
DECISIONS.mdADR-017 (the current AI-assistance boundary — supersedes the older ADR-003). -
REVERSE_ENGINEERING.md's own template and status legend.
- Find the package name and installed version:
adb shell dumpsys package com.google.android.apps.wearables.maestro.companion | grep -E "versionName|versionCode"
- Find the APK path(s) — a companion app this size may be split (base + density/language/ABI
splits), so
pm pathcan legitimately return more than one line. Pull all of them:adb shell pm path com.google.android.apps.wearables.maestro.companion # package:/data/app/.../base.apk # package:/data/app/.../split_config.xxhdpi.apk <- pull this one too, if present mkdir -p "reverse-engineering/apk/v<versionName>-<versionCode>/" adb pull /data/app/.../base.apk "reverse-engineering/apk/v<versionName>-<versionCode>/" adb pull /data/app/.../split_config.xxhdpi.apk "reverse-engineering/apk/v<versionName>-<versionCode>/" # repeat per split
- Hash every pulled file and record the result:
sha256sum reverse-engineering/apk/v<versionName>-<versionCode>/*.apk
- [Maintainer decision — recording provenance is mechanical, but confirm the row is accurate]
Add a row to
reverse-engineering/APK_VERSIONS.md's table: version dir, file(s), SHA-256 per file, versionName/versionCode, pull date, source device, and provenance (how it was obtained — e.g. "Play Store, maintainer's own Google account, installed on the maintainer's own device"). - Remember: nothing under
reverse-engineering/apk/is ever committed — it's fully covered by.gitignore(the APK itself,jadx-output/,apktool-output/,pbtk-output/). Only thereverse-engineering/APK_VERSIONS.mdrow you just added is git-tracked.
A new companion-app release doesn't mean starting analysis from zero. Before decompiling from scratch:
- If a previous version's
apktool-output/still exists locally, diff the two versions' DEX class lists (e.g.diff <(unzip -l v<old>/base.apk | grep '\.dex$') <(unzip -l v<new>/base.apk | grep '\.dex$'), or compareapktool's ownsmali/directory trees after decompiling both) to see whether the set of classes changed at all before re-running a full keyword search. - Re-check every class/finding already recorded in
REVERSE_ENGINEERING.md's "Identified relevant classes" section and "Correlation status withPROTOCOL.md" table against the new version — note whether each one still exists, moved, or was removed/renamed, rather than assuming prior findings still apply unchanged. - [Maintainer decision] Whether a shifted/renamed class still represents the same behavior (and
whether any existing
PROTOCOL.mdFACT needs re-verification against the new version) is a relevance judgment — flag candidates, don't silently carry forward or silently invalidate a prior finding.
cd "reverse-engineering/apk/v<versionName>-<versionCode>/"
# Readable Kotlin/Java (primary source for keyword search and citations)
jadx -d jadx-output/ base.apk
# Resources, manifest, smali (fallback when JADX misdecompiles something)
apktool d base.apk -o apktool-output/
# .proto schema extraction (see WORKSTATION_PREPARATIONS.md for the real, confirmed pbtk scope —
# it is NOT limited to Java/DEX; pbtk-from-binary targets native .so files too, though whether it
# actually succeeds against libmaestro/libgfps's specific binaries is unconfirmed until tried)
pbtk-jar-extract base.apk pbtk-output/
pbtk-from-binary apktool-output/lib/<abi>/libmaestro.so pbtk-output/ # once §4's native .so pass locates itRecord the exact tool versions used (jadx --version, apktool --version, pbtk's pipx list
output) in reverse-engineering/APK_VERSIONS.md's row for this version — decompiler output (line
numbers, class layout) can shift between versions, so a file+line citation is only reproducible if
the decompiler version is pinned too.
This is the mechanical step ADR-017 permits an AI session to run directly. Search results are
candidates, not findings — every candidate goes to the maintainer for the relevance call before
anything is recorded in REVERSE_ENGINEERING.md.
- Start from
AndroidManifest.xml(apktool-output/AndroidManifest.xml, not the obfuscated binary form) — find services/receivers registered against Bluetooth-related intents/actions first. This anchors which classes are worth reading closely before any blind keyword search. - Search for string literals, not just class/method names — log tags, notification channel names, broadcast action strings, and SharedPreferences keys survive ProGuard/R8 obfuscation even when class names don't (obfuscation renames identifiers, not string constants).
- Check
qzed/pbpctrl's public documentation first, as a research accelerant — protocol knowledge only, perAGENTS.md§12/README.md's attribution boundary (never copy code; every candidate this surfaces still needs independent re-verification against this project's own APK/captures, not taken onpbpctrl's word). - Search
jadx-output/for the keyword list inREVERSE_ENGINEERING.md§Method (BLE/GATT classes, RFCOMM/Fast-Pair classes, HID classes, package-name fragments,.proto-generated-class markers). - Apply the exclusion list below during this step itself — don't collect candidates from these areas and filter them out later; skip past them at search time.
- Present candidates to the maintainer: file path, line number, the matched keyword/string, and a
one-line note on why it looked relevant. [Maintainer decision] which candidates get written up
in
REVERSE_ENGINEERING.md, and at what confidence tier.
A fully decompiled app exposes far more surface area than any single capture. The following are
explicitly out of scope (PROJECT.md non-goals, DECISIONS.md ADR-008) — do not spend search
or read time on them even if they surface incidentally:
AccountLinking(any package/class matching this name)OwnershipTransfer(any package/class matching this name)AccessoryNonOwner(the Accessory Non-Owner Service — see ADR-008)- Any Firebase-, Analytics-, or Crashlytics-named package or class (
com.google.firebase.*,*Analytics*,*Crashlytics*, or equivalent — seeAGENTS.md§1's Zero-GMS rule)
If one of these surfaces incidentally while searching for something else, note it as
"out-of-scope, skipped" (mirroring how DECISIONS.md ADR-008 already treats incidental
Account-Linking/Non-Owner traffic in captures) and move on — do not follow the call graph into it.
- Write up each maintainer-approved candidate in
REVERSE_ENGINEERING.md's "Identified relevant classes" section, using its template — every finding cites the exact decompiled file and line number (e.g.jadx-output/sources/com/google/.../Xy2.java:142), not only a class name. - Label every finding FACT / HYPOTHESIS / ASSUMPTION / OPEN QUESTION per
PROJECT_RULES.md§1 — static analysis alone is never 🟢 FACT for a protocol claim (only for "this code exists and looks like X"); a protocol-behavior claim needs capture correlation first. - For every HYPOTHESIS recorded, add a short note on how it could be confirmed or refuted against a
capture — which action/Test-ID (
TESTPLAN_BLUETOOTH_HCI_SNOOP.md) would need to be captured — perREVERSE_ENGINEERING.md's template (seePROJECT_RULES.md§4's hypothesis-test discipline, already used for captures). - Once a finding is cross-checked against a real capture, promote it directly into
PROTOCOL.md— there is no intermediate working-notes buffer (PROTOCOL_NOTES.mdwas retired 2026-08-15, seePROJECT_RULES.md§2 rule 5). UpdateREVERSE_ENGINEERING.md's "Correlation status withPROTOCOL.md" table so the same finding is never independently "rediscovered" in both documents. - [Maintainer decision, per
AGENTS.md§6] Promoting anything to 🟢 FACT inPROTOCOL.md, or writing/superseding aDECISIONS.mdADR, still requires explicit maintainer sign-off — an AI session may propose and draft, never commit either as settled.
- Never hand-reconstruct a
.protoschema from getter/setter names alone — field names recovered from JADX are not proof of the actual wire field numbers, which determine binary compatibility. Usepbtk's actual extraction output (§3), not a guessed schema. - JADX can misdecompile obfuscated/optimized constructs — when a decompiled method looks
suspicious or incomplete, cross-check against the
apktoolsmali output before trusting it. - Reflection-based code stays invisible to static analysis — if a call site is never found
despite a class clearly needing one, that's a sign dynamic analysis (Frida) may be needed; log any
such experiment in the relevant capture's
CAP-NNN-FINDINGS.mdfirst, perPROJECT_RULES.md§4. - Native
.sodisassembly is in scope for AI mechanical assistance (DECISIONS.mdADR-017 §4), on the same terms as DEX/Java work: search, list, and explain disassembly output; never decide relevance or promote a finding. Whetherpbtk-from-binaryactually succeeds againstlibmaestro/libgfps's specific binaries (vs. a stripped protobuf-lite descriptor pool) is unconfirmed until tried — seeWORKSTATION_PREPARATIONS.md. - A class or method existing in the APK does not prove it's exercised by any specific action in
TESTPLAN_BLUETOOTH_HCI_SNOOP.md— treat every static finding as 🟡 HYPOTHESIS until a capture shows the corresponding traffic (REVERSE_ENGINEERING.md's own "Known limitations" section). - Nothing under
reverse-engineering/apk/is ever committed — not the APK, notjadx-output/, notapktool-output/, notpbtk-output/. Onlyreverse-engineering/APK_VERSIONS.mdand this procedure document are git-tracked. Double-checkgit statusbefore committing anything from a reverse-engineering session. - Never independently promote a finding to 🟢 FACT, and never commit a new/superseding
DECISIONS.mdADR —AGENTS.md§6/§15, unaffected by ADR-017's mechanical-assistance boundary.
https://github.com/tedsluis/opencontrolpixelbudspro2/blob/main/APK_REVERSE_ENGINEERING_PROCEDURE.md - https://tedsluis.github.io/opencontrolpixelbudspro2/APK_REVERSE_ENGINEERING_PROCEDURE