Skip to content

← Firmware Downloads

Post-install setup · Koensayr · Y1

Bluetooth profile-stack spec compliance

A Y1 custom Software maintained by the community.

Instructions published by the project, shown here word for word.

Bluetooth profile-stack spec compliance

Current spec compliance state across the Bluetooth profile stack the Y1 talks to peer Controllers (CTs) over. AVRCP 1.3 is the primary target — full ICS Table 7 scorecard with M/O accounting in §2 — because that's where most of the Y1's stock behaviour was non-conformant. Adjacent profiles (A2DP, AVDTP, AVCTP, GAVDP) are in scope when their behaviour bleeds into AVRCP-level CT interop, with targeted fixes per §9 rather than full per-profile compliance scoreboards. Per-CT empirical observations behind each design choice live in INVESTIGATION.md "Hardware test history per CT".

For why we have a proxy at all, the current trampoline chain shape, and the calling conventions of the response builders we use, see ARCHITECTURE.md. For per-patch byte-level reference, see PATCHES.md.


0. Spec target + citation discipline

Primary wire protocol target: AVRCP 1.3 (V13, adopted 16 April 2007), with ESR07 errata applied. AVCTP 1.2 paired per §6 SDP record. Canonical PDFs live under docs/spec/<PROFILE>/ (gitignored — Bluetooth SIG copyright disallows redistribution; download from https://www.bluetooth.com/specifications/specs/):

AVRCP 1.3 (primary):

AVCTP 1.2 (paired, §6 + ESR07 §2.1 / Erratum 4969):

A2DP / AVDTP / GAVDP triad (audio path, §9 references):

Per ICS §1.2 Table 2b, AVRCP 1.3 was deprecated 2023-02-01 and is scheduled for withdrawal 2027-02-01. We're patching a 2012 firmware that was originally qualified against AVRCP 1.0; the deprecation schedule does not block us from shipping.

Citation hygiene rule. Cite by PDU name + AVRCP 1.3 section number verified against the spec PDF's table-of-contents. Where a behavior comes from AV/C Panel Subunit Spec (PASS THROUGH op codes / press-release semantics), cite as AVRCP 1.3 §4.6.1 (defined in AV/C Panel Subunit Spec, ref [2]). Where the spec text contains a printed typo, cite ESR07's clarification: AVRCP 1.3 §X.Y + ESR07 §2.2. Section numbers must appear in the spec PDF's table of contents — anything else is a citation error. Adjacent-profile citations follow the same shape: A2DP 1.3 §X.Y, AVDTP 1.3 §X.Y, AVCTP 1.2 §X.Y, GAVDP 1.3 §X.Y.


1. Current coverage state

AVRCP 1.3 (primary target). The goal is AVRCP 1.3 spec-completeness so any spec-compliant 1.3+ controller renders our metadata. Scope is the latest revision of the AVRCP 1.3 spec (V13 + ESR07). Anything outside that revision is out of scope for the AVRCP scoreboard.

The one carry-out from outside the 1.3 spec proper is MtkBt.odex patch F1's BlueAngel-internal getPreferVersion() value — internal flag bookkeeping inside MtkBt's Java-side dispatcher that unblocks 1.3+ command handling on a stack that was originally compiled against AVRCP 1.0. F1 sets the flag and unblocks 1.3 dispatch; nothing in our wire shape is changed by it.

Lower BT profile stack (adjacent). A2DP, AVDTP, AVCTP, GAVDP fixes are filed in §9. Goal: spec-completeness where tractable on this hardware (every Optional row we can implement, not just observed-deviation fixes). Each entry in §9 has motivation, scope, and a verification path.

Closed: every Mandatory row of ICS Table 7, plus every Optional row.

PlayerApplicationSettings (PDUs 0x11-0x16 + event 0x08, ICS Table 7 rows 12-17 + 30). T_papp dispatches PDUs 0x11-0x16 internally; T8 emits event 0x08 INTERIM, T9 emits proactive CHANGED on edge. Set loop (CT→Y1): T_papp 0x14 validates the inbound (attr_id, value) against the supported set, writes the pair to /data/data/com.innioasis.y1/files/y1-papp-set, ACKs with success; the music app's PappSetFileObserver consumes the CLOSE_WRITE, translates AVRCP→Y1 enum, and calls SharedPreferencesUtils.setMusicRepeatMode/setMusicIsShuffle directly. CHANGED loop (Y1→CT): Patch B4's com.koensayr.PappStateBroadcaster registers as OnSharedPreferenceChangeListener against the "settings" SharedPreferences, calls TrackInfoWriter.setPapp(repeat, shuffle) to update y1-track-info[795..796], and triggers a playstatechanged wake whenever musicRepeatMode or musicIsShuffle flips (covering both CT-driven Sets and Y1-UI toggles uniformly). T9's edge detector then emits reg_notievent_player_appsettings_changed_rsp (PLT 0x345c) with REASON_CHANGED. T8 0x08 INTERIM and T9 papp CHANGED both read live [795..796] so a fresh CT subscribe sees actual current state.


2. Coverage matrix — current vs spec

Anchored against ICS Table 7 (Target Features) in docs/spec/AVRCP 1.3/AVRCP.ICS.p17.pdf §1.5, which is the canonical M/O determination. M/O status is conditional on what other features the TG claims; the ICS encodes the conditionals explicitly. PDU = "PDU ID" byte at AV/C body offset +4. AVRCP 1.3 V13 spec sections in docs/spec/AVRCP 1.3/AVRCP_SPEC_V13.pdf.

Our claims that drive the conditionals: PASS THROUGH Cat 1 (V1 SDP record, ICS Table 7 item 7), GetElementAttributes Response (T4, item 20). Combining these:

ICS Table 7 rowPDU / CapabilitySpec §ICS Status (this proj)Currently shippedGap
2Accepting connection establishment (control)§4.1.1M✓ (mtkbt)—
3-4Connection release§4.1.2M✓ (mtkbt)—
5Receiving UNIT INFO§4.1.3M✓ (mtkbt)—
6Receiving SUBUNIT INFO§4.1.3M✓ (mtkbt)—
7Receiving PASS THROUGH cat 1§4.1.3M (C.1: at least one cat)✓ (mtkbt + Patch E)—
8Receiving PASS THROUGH cat 2§4.1.3C.1: not required (cat 1 satisfies)not claimed—
9Receiving PASS THROUGH cat 3§4.1.3C.1: not requirednot claimed—
10Receiving PASS THROUGH cat 4§4.1.3C.1: not requirednot claimed—
11GetCapabilities Response (PDU 0x10)§5.1.1M (C.3: M IF cat 1)✓ T1—
12-15List / Get / Set PApp Settings (0x11–0x14)§5.2.1–5.2.4C.14: M to support none or all✓ T_papp (Repeat + Shuffle, OFF/OFF; Set rejects 0x06 INTERNAL_ERROR — see §1)—
16-17PApp Setting Attribute / Value Text (0x15-0x16)§5.2.5-5.2.6O✓ T_papp (UTF-8 "Repeat" / "Shuffle" / "Off")—
18InformDisplayableCharacterSet (PDU 0x17)§5.2.7O✓ T_charset (NOT_IMPLEMENTED reject; spec-permissible for Optional PDU)—
19InformBatteryStatusOfCT (PDU 0x18)§5.2.8O✓ T_battery—
20GetElementAttributes (PDU 0x20)§5.3.1M (C.3: M IF cat 1)✓ T4 (all 7 Appendix E attrs: Title / Artist / Album / TrackNumber / TotalNumberOfTracks / Genre / PlayingTime, single packed frame)—
21GetPlayStatus (PDU 0x30)§5.4.1M (C.2: M IF GetElementAttributes Response)✓ T6 with live position via clock_gettime(CLOCK_BOOTTIME)—
22RegisterNotification (PDU 0x31)§5.4.2M (C.12: M IF cat 1)✓ T2 / extended_T2 / T8—
23Notify EVENT_PLAYBACK_STATUS_CHANGED§5.4.2 Tbl 5.29M (C.4: M IF GetElementAttributes + RegisterNotification)✓ T8 INTERIM + T9 CHANGED on edge—
24Notify EVENT_TRACK_CHANGED§5.4.2 Tbl 5.30M (C.4)✓ extended_T2 INTERIM + T5 CHANGED on edge—
25Notify EVENT_TRACK_REACHED_END§5.4.2 Tbl 5.31O✓ T8 INTERIM + T5 CHANGED-on-edge (gated on natural-end flag set by music app's PlaybackStateBridge.onCompletion)—
26Notify EVENT_TRACK_REACHED_START§5.4.2 Tbl 5.32O✓ T8 INTERIM + T5 CHANGED-on-edge (unconditional on every track edge)—
27Notify EVENT_PLAYBACK_POS_CHANGED§5.4.2 Tbl 5.33O✓ T8 INTERIM + T9 CHANGED at 1 s cadence while playing (1 s tick fires playstatechanged; T9 live-extrapolates position via clock_gettime(CLOCK_BOOTTIME))—
28Notify EVENT_BATT_STATUS_CHANGED§5.4.2 Tbl 5.34O✓ T8 INTERIM reads y1-track-info[794] (real bucket from Intent.ACTION_BATTERY_CHANGED) + T9 CHANGED-on-edge piggybacked on playstatechanged broadcast—
29Notify EVENT_SYSTEM_STATUS_CHANGED§5.4.2 Tbl 5.36O✓ T8 INTERIM with 0x00 POWER_ON (canned IS the real value while trampolines run — see footnote†)—
30Notify EVENT_PLAYER_APPLICATION_SETTING_CHANGED§5.4.2 Tbl 5.37O✓ T8 INTERIM + T9 proactive CHANGED, both reading live (Repeat, Shuffle) from y1-track-info[795..796] (Patch B4 PappStateBroadcaster → TrackInfoWriter.setPapp + playstatechanged → T9 edge detect)—
31-32Continuation (PDUs 0x40/0x41)§5.5C.2: M IF GetElementAttributes Response✓ T_continuation explicit dispatch in T4 pre-check → AV/C NOT_IMPLEMENTED reject via UNKNOW_INDICATION path (msg=520)—
65Discoverable Mode§12.1M✓ (mtkbt)—
66PASSTHROUGH operation supporting Press and Hold§4.1.3O✓ (mtkbt + U1 disables kernel auto-repeat on AVRCP uinput)—

Mandatory rows: all hit. Optional rows: all hit. ICS Table 7 (Target Features) is fully covered.

† Event 0x07 SYSTEM_STATUS_CHANGED — INTERIM-only is intentional. While trampolines execute, the system is by definition POWER_ON; UNPLUGGED is for accessory / dock contexts that don't apply to the Y1; POWER_OFF is unobservable from inside a process that can no longer emit responses. The canned 0x00 POWER_ON value IS the real value, so there is no edge to fire CHANGED on.

INTERIM vs. CHANGED notation reminder. AVRCP 1.3 §5.4.2 splits each event subscription into two response shapes: an immediate INTERIM carrying the current value at registration time, and an asynchronous CHANGED when the relevant condition fires. Mandatory rows 23 and 24 ship both halves. Optional rows 25 / 26 / 27 / 28 also ship both halves with real-data CHANGED-on-edge. Row 29 SYSTEM_STATUS_CHANGED ships INTERIM only (see footnote above).

ICS Table 8 (Cat 1 PASSTHROUGH op_ids — mandatory subset):

op_idOperationICS statusCurrently shipped
0x44Play (item 19)M✓ Patch E → play(true)
0x45Stop (item 20)M✓ Patch E → stop()V
0x46Pause (item 21)O✓ Patch E → pause(0x12, true)
0x48Rewind (item 23)Opartial (mtkbt fftimer + Y1 long-press)
0x49Fast forward (item 24)Opartial
0x4BForward / next track (item 26)O✓
0x4CBackward / prev track (item 27)O✓

Mandatory cat 1 op_ids: both hit (PLAY + STOP).


Notification events (PDU 0x31 sub-dispatch, AVRCP 1.3 §5.4.2 Tables 5.29–5.37)

The advertised set in the GetCapabilities response (T1's EventsSupported array) determines what a CT can register for. We advertise eight events: {0x01, 0x02, 0x05, 0x08, 0x09, 0x0a, 0x0b, 0x0c}. Events 0x03, 0x04, 0x06, 0x07 are not advertised; T8 / T5 / T9 still handle them if a permissive CT subscribes anyway.

Subscription gating + §5.4.2 CHANGED cadence: every inbound RegisterNotification CMD writes g_avrcp_req_event_database[event_id] = seq_id + 1 (vaddr 0xd2b5, .bss, 15 bytes — one byte per event_id). A nonzero byte means "subscribed in this session" and "this is the inbound AVCTP TID to echo on the matching CHANGED." T5 / T9 read the table at every CHANGED emit site and skip when the byte is zero. The table is zeroed by clear_event_database on every T1 GetCapabilities so a fresh CT-connection cannot inherit stale subscriptions from a previous CT. Within a session, CHANGED emits are continuous — the gate is not cleared after emit — letting Y1's per-edge / per-tick wire cadence match the music app's broadcast rate rather than each CT's individual re-RegisterNotification cadence. M1 widens the cmp constant in mtkbt's RegNotif response dispatch (fcn.0x121d8 at 0x12230) from 1 to 0x0F, so the dispatcher routes the JNI's reasonCode byte (IPC offset 8 = ctxt[8]) to the matching wire ctype: INTERIM (0x0F) for first-response arms, CHANGED (0x0D) for edge emits.

AVRCP 1.3 §4.2.1 strict TID echo on non-RegNotif PDUs: every response Y1 emits must echo the inbound CMD's AVCTP transaction label. For RegNotif INTERIM/CHANGED the database mechanism above supplies the TID. For non-RegNotif responses (GetEA / GetPlayStatus / Charset / Battery / PApp / Continuation), T4's prologue writes conn[+0x11] = sp[+0x171] (the inbound CMD's seq_id at the empirically-verified pre-SUB SP stack offset). All response builders read conn[+0x11] and pack it into the outbound AVCTP TL via the wire-emit chain conn[+0x11] → msg[5] → packet[+0xa] → chan+0x39 → wire byte 0. The same chain serves GetCapabilities responses via stock JNI's pre-R1 path, which writes conn[+0x11] before our hijack takes effect.

event_idNameSpec §INTERIMCHANGED on edge
0x01PLAYBACK_STATUS_CHANGED§5.4.2 Tbl 5.29✓ T8✓ T9 on play/pause edge (gated on database[1] != 0)
0x02TRACK_CHANGED§5.4.2 Tbl 5.30✓ extended_T2✓ T5 on track edge (gated on database[2] != 0)
0x05PLAYBACK_POS_CHANGED§5.4.2 Tbl 5.33✓ T8✓ T9 at ~1Hz while playing + T5 on track edge (gated on database[5] != 0)
0x08PLAYER_APPLICATION_SETTING_CHANGED§5.4.2 Tbl 5.37✓ T8 (reads y1-track-info[795..796])✓ T9 on Repeat/Shuffle edge (gated on database[8] != 0)
0x09NOW_PLAYING_CONTENT_CHANGED1.4+ event ID✓ T8 (zero/empty payload)✓ T5 on track edge + T9 on play/pause edge (gated on database[9] != 0)
0x0aAVAILABLE_PLAYERS_CHANGED1.4+ event ID✓ T8 (zero/empty payload)n/a (Y1 has one player)
0x0bADDRESSED_PLAYER_CHANGED1.4+ event ID✓ T8 (PlayerID=0, UidCtr=0)n/a (Y1 has one player)
0x0cUIDS_CHANGED1.4+ event ID✓ T8 (UidCtr=0)n/a (Y1 has no UID database)

3. Binary discovery — what's already mapped

objdump -dRT against the stock libraries (byte-identical between v3.0.2 and v3.0.7) was the entire discovery pass for what response-builder primitives we have to call. Result: every PDU and every event has both:

Full PLT inventory (from libextavrcp_jni.so md5 fd2ce74db9389980b55bccf3d8f15660, what each trampoline would blx):

PDU / eventResponse builderPLT @libextavrcp.so body @
Currently used by shipped trampolines
0x10 GetCapabilitiesget_capabilities_rsp0x35dc0x1dac
0x17 InformDisplayableCharacterSet(T_charset rejects via UNKNOW_INDICATION; ACK builder inform_charsetset_rsp at PLT 0x3588 / body 0x2138 is no longer called)——
0x18 InformBatteryStatusOfCTbattery_status_rsp0x357c0x2160
0x20 GetElementAttributesget_element_attributes_rsp0x35700x2188
0x30 GetPlayStatusget_playstatus_rsp0x35640x2354
0x31 event 0x01reg_notievent_playback_rsp0x339c0x23f0
0x31 event 0x02reg_notievent_track_changed_rsp0x33840x2458
0x31 event 0x03reg_notievent_reached_end_rsp0x33780x24c8
0x31 event 0x04reg_notievent_reached_start_rsp0x336c0x2528
0x31 event 0x05reg_notievent_pos_changed_rsp0x33600x2588
0x31 event 0x06reg_notievent_battery_status_changed_rsp0x33540x25f0
0x31 event 0x07reg_notievent_system_status_changed_rsp0x33480x2658
0x31 event 0x09reg_notievent_now_playing_content_changed_rsp0x330c0x26c0
0x31 event 0x0areg_notievent_availplayers_changed_rsp0x33240x27b0
0x31 event 0x0breg_notievent_addredplayer_changed_rsp0x33300x2810
0x31 event 0x0creg_notievent_uids_changed_rsp0x33180x2880
(default reject for unknown PDU including 0x40/0x41)pass_through_rsp0x3624n/a (in libextavrcp.so too — reached via UNKNOW_INDICATION fall-through)
PlayerApplicationSettings (T_papp + T8 event 0x08)
0x11list_player_attrs_rsp0x35d00x1e24
0x12list_player_values_rsp0x35c40x1e74
0x13get_curplayer_value_rsp0x35b80x1ed0
0x14set_player_value_rsp0x35940x1f2e
0x15get_player_attr_text_rsp0x35ac0x1f58
0x16get_player_value_text_value_rsp0x35a00x203c
0x31 event 0x08reg_notievent_player_appsettings_changed_rsp0x345c0x2720

No new PLT discovery needed. All builders linked at stock and exercised by the trampoline chain. Calling-convention reference: ARCHITECTURE.md PApp builder table.

Code-cave budget

LOAD #1 padding budget: 4020 bytes total (0xac54..0xbc08). The patcher computes the assembled trampoline blob size at build time and prints it on every run; release builds currently land in the low 3 kB range, debug builds (KOENSAYR_DEBUG=1) ~150 B larger from the spliced __android_log_print calls. The patcher asserts on overflow before it can corrupt LOAD #2's .data / .got.

If we ever do exhaust LOAD #1 padding, the fallback is to extend the same trick to the LOAD #2 padding region by bumping LOAD #2's p_filesz/p_memsz.


4. y1-track-info schema (cumulative)

File-level wrapper:

File offsetSizeContents
01 Bactive_slot — single-byte indicator (0 or 1). Reader's atomic load → dispatch. Writer's atomic store after slot fill flips this.
1..33 BRFA padding (4-B align slot[0]).
4..11071104 Bslot[0] — 1104-byte schema below (in-place when active_slot == 0 on read, fresh write target when active_slot == 1).
1108..22111104 Bslot[1] — second copy.
22121 BRFA padding.

Total file: 2213 B. Reader (libextavrcp_jni.so trampolines via get_or_init_mmap + read_track_info) memory-maps the file once per process, reads file[0] per emit to pick the slot, byte-copies the 1104-byte payload (or any sub-range via slot_offset) into the trampoline's stack file_buf. Writer (TrackInfoWriter.flushLocked) writes the inactive slot via RandomAccessFile.seek+write, then atomically flips file[0] to point at the just-written slot — readers never observe a torn slot.

Per-slot schema (offsets relative to the active slot's start):

OffsetFieldSizeStatusSource
0..7track_id (synthetic)8shippedSong.getId() or syntheticAudioId(path) fallback (read live from PlayerService.getCurrentSong() in TrackInfoWriter.flushLocked)
8..263Title256shippedSong.getName() (read live from PlayerService.getCurrentSong())
264..519Artist256shippedSong.getArtist() (read live from PlayerService.getCurrentSong())
520..775Album256shippedSong.getAlbum() (read live from PlayerService.getCurrentSong())
776..779duration_ms (BE u32)4shippedSong.getDuration() (read live from PlayerService.getCurrentSong())
780..783position_at_state_change_ms (BE u32)4shippedTrackInfoWriter.mPositionAtStateChange
784..787state_change_time_ms (BE u32)4shippedTrackInfoWriter.mStateChangeTime (CLOCK_BOOTTIME ms-since-boot, full ms precision; T6 / T9 live-position extrapolation. tv_nsec/1e6 computed in-trampoline via magic-multiply)
788..791reserved4—(pad)
792playing_flag1shippedTrackInfoWriter.mPlayStatus (3-valued AVRCP §5.4.1 Tbl 5.26 enum: 0=STOPPED, 1=PLAYING, 2=PAUSED — set by PlaybackStateBridge.onPlayValue hooking Static.setPlayValue newValue 0/1/3/5)
793previous_track_natural_end1shippedTrackInfoWriter.mPreviousTrackNaturalEnd (T5 gate for AVRCP §5.4.2 Tbl 5.31 TRACK_REACHED_END CHANGED)
794battery_status1shippedTrackInfoWriter.mBatteryStatus (T8 INTERIM + T9 CHANGED-on-edge for AVRCP §5.4.2 Tbl 5.34 BATT_STATUS_CHANGED)
795repeat_avrcp1shippedTrackInfoWriter.mRepeatAvrcp (AVRCP 1.3 Appendix F attribute 0x02 enum; written by PappSetFileObserver on y1-papp-set writes; T8 0x08 INTERIM + T9 papp CHANGED-on-edge read this byte)
796shuffle_avrcp1shippedTrackInfoWriter.mShuffleAvrcp (AVRCP 1.3 Appendix F attribute 0x03 enum; same write/read pipeline as 795)
797..799reserved3—available for future PApp attribute additions (Equalizer / Scan if a Y1 release ever surfaces them)
800..815TrackNumber (UTF-8 ASCII decimal)16shippedMediaStore.Audio.Media.TRACK % 1000 / parsed from METADATA_KEY_CD_TRACK_NUMBER
816..831TotalNumberOfTracks (UTF-8 ASCII decimal)16shippedcount(*) WHERE ALBUM_ID=? / parsed from CD_TRACK_NUMBER "n/total"
832..847PlayingTime (UTF-8 ASCII decimal ms)16shippedderived from duration_ms
848..1103Genre (UTF-8)256shippedMediaStore.Audio.Genres / METADATA_KEY_GENRE

Total slot size: 1104 B. Per-slot schema is append-only; we never relocate existing fields. The file-level wrapper (1 B active_slot + 3 B pad + 2 × 1104 B slots + 1 B pad = 2213 B) keeps reader / writer race-free without tmpfile + rename.

The numeric AVRCP 1.3 Appendix E attrs (4 / 5 / 7) are stored pre-formatted as ASCII decimal strings rather than binary u16 / u32 with a Thumb-2 itoa, keeping the T4 trampoline a uniform strlen+memcpy loop.

Trampoline edge state lives in libextavrcp_jni.so .bss at G_Y1_TRAMPOLINE_STATE_VADDR = 0xd2d6 (13 B): bytes 0..7 = last_seen track_id (T5 / T4 edge detect), byte 8 = unused (was last RegNotif transId; per-event TIDs now live in g_avrcp_req_event_database), byte 9 = last_play_status (T9), byte 10 = last_battery_status (T9), byte 11 = last_repeat_avrcp (T9 papp), byte 12 = last_shuffle_avrcp (T9 papp). Zero-init at process load (same scope as g_avrcp_req_event_database).


5. PlayerApplicationSettings — response-builder discovery

Discovery work that will be required before trampolines for PDUs 0x11-0x16 + event 0x08 can be written:

Functionlibextavrcp.so @Notes
list_player_attrs_rsp0x1e24PDU 0x11 — return list of supported attribute IDs
list_player_values_rsp0x1e74PDU 0x12 — return allowed values for one attribute
get_curplayer_value_rsp0x1ed0PDU 0x13 — return current values for the requested attribute IDs
set_player_value_rsp0x1f2ePDU 0x14 — accept new value (also requires a write path that mutates Y1's SharedPreferences from a different process)
get_player_attr_text_rsp0x1f58PDU 0x15 — return UTF-8 attribute label text
get_player_value_text_value_rsp0x203cPDU 0x16 — return UTF-8 value label text
reg_notievent_player_appsettings_changed_rsp0x2720event 0x08 — proactive CHANGED on shuffle / repeat edges

Recipe per function: see ARCHITECTURE.md §"Adding a new PDU handler". The argument shape must be confirmed via disassembly, not inferred from arg name — the OEM names don't always match semantics (see the worked example on get_element_attributes_rsp).

Sub-PDU detail (informative):

Plus: proactive CHANGED on shuffle / repeat changes via event 0x08, fed by the same broadcast receivers.


6. Test / verification strategy

Per change: a hardware capture against at least three CTs covering different policy postures. Test-matrix CT roster + per-CT empirical observations live in INVESTIGATION.md "Hardware test history per CT". Recommended posture spread:

For each CT, capture btlog+logcat with:

./tools/dual-capture.sh /work/logs/dual-<ct>-<run>/

and verify:

  1. No new NACKs. msg=520 NOT_IMPLEMENTED count should drop to zero (or stay at zero).
  2. Expected msg=N response sizes. Each PDU has a known wire-shape; its outbound IPC frame size should be deterministic.
  3. EventsSupported announcement. T1's GetCapabilities response advertises every event the current build can satisfy; CT either subscribes or it doesn't, but it shouldn't subscribe to events we can't satisfy and then NACK.
  4. Battery footprint. dmesg-after wake-lock count and getprop battery stats should not regress.

The btlog parser (tools/btlog-parse.py) gives us full HCI command/event visibility. Any AVCTP-level NACK or rejection will surface there as a result: field on the relevant CNF.


7. Risk register (when extending)

RiskLikelihoodMitigation
New PDU response builder has an arg convention not derivable from disassembly aloneMediumBisect via JNI in-tree caller (most builders are called by stock JNI somewhere even if Java stack never reaches them). Failing that, ship a no-op trampoline that returns NOT_IMPLEMENTED and watch CT behavior to confirm the CT was actually probing for that PDU.
The 1 s PLAYBACK_POS_CHANGED cadence creates wakeup pressureLowT9's position-emit block runs only when file[792]==PLAYING and is driven by a 1 s Handler.postDelayed loop that's cancelled the moment playback pauses or stops. No timer fires while idle. Strict CTs subscribed for a longer interval are over-served (spec-permissible — shall be emitted at this interval defines a max-interval ceiling, not a min cadence floor).
LOAD #1 extension exhausts page-paddingVery low4020-byte cap is hard-asserted in the patcher; build fails before it can corrupt LOAD #2's GOT. Fallback: extend LOAD #2 padding.
Trampoline blob shifts every PLT call beyond rangeLow (Thumb b.w covers ±16 MB; trampolines and PLT are <0x10000 apart)Verify bl.w/b.w reach in _thumb2asm.py self-test for each new emit site.
AVRCP version negotiation: F1 patch sets MtkBt-internal version flag but our wire-shape PDU set is 1.3LowF1 only flips the BlueAngel-internal flag to unblock 1.3+ command dispatch through MtkBt's Java layer. SDP record advertises AVRCP 1.3 (V1 patch) / AVCTP 1.2 (V2 patch). Per AVRCP 1.3 §6 (Service Discovery Interoperability Requirements) + ESR07 §2.1 / Erratum 4969, the served version is what CTs key against, and they negotiate a 1.3 dialogue — which is what we implement.
Continuation PDU 0x40/0x41 traffic from a CT (would require stateful re-emit)Low — TG never fragmentsT_continuation emits NOT_IMPLEMENTED, which is spec-acceptable. If a CT ever issues 0x40, upgrade to a stateful re-emitter.
AVCTP saturation under a CT subscribe storm drops PASSTHROUGH key-release frames; the music app then interprets the held key as a long-press, calls startFastForward()/startRewind(), and the lambda thread runs foreverMedium (high-subscribe-rate CT classes have been observed driving this — see INVESTIGATION.md for per-CT empirical context)U1: NOP UI_SET_EVBIT(EV_REP) at libextavrcp_jni.so:0x74e8 so the kernel's evdev soft-repeat timer never fires on the AVRCP virtual keyboard. Without auto-repeat, a dropped PASSTHROUGH RELEASE can no longer drive the held-key cascade; the music app sees one event per actual PRESS frame the CT sends. Spec-correct per AVRCP 1.3 §4.6.1 + AV/C Panel Subunit Spec (CT periodic re-send during held button).

8. Out of scope (and why)

Anything outside the AVRCP 1.3 spec proper (V13 + ESR07) is out of scope for this project. See §1 above.


9. Lower BT profile stack

AVRCP 1.3 sits on top of AVCTP, which rides L2CAP. A2DP rides AVDTP, signalled by GAVDP. All four lower profiles live in mtkbt (BlueAngel internal stack — see ARCHITECTURE.md "Lower BT profile stack").

Scope. Spec-completeness where tractable on this hardware — every Optional row we can implement, not just observed-deviation fixes. Per the [feedback_y1_upstream_spec_compliance] memory rule, fixes go at the spec-deviation layer, not via AVRCP-level workarounds. Subsections are organised per-issue with the affected profile noted in the heading.

9.1 AVRCP META CONTINUING_RESPONSE (PDUs 0x40 / 0x41) — AVRCP / AVCTP — DEFERRED

Spec. AVRCP 1.3 §5.5: when a TG response exceeds CT buffer / AVCTP MTU, TG sends packet_type=01 START; CT responds with PDU 0x40 (Continuing) or 0x41 (Abort). ICS Table 7 rows 31-32: Mandatory if GetElementAttributes Response is supported (C.2).

Current state. T_continuation rejects 0x40 / 0x41 with AV/C NOT_IMPLEMENTED via the UNKNOW_INDICATION fallback. Counted as ✓ in §2 because the OEM _send_get_element_attributes_rsp packs into a 644 B buffer and emits a single non-fragmented frame — TG never sets packet_type=01, so a spec-conforming CT never sends 0x40.

Why deferred. A stateful re-emitter is only meaningful after the OEM _send_get_element_attributes_rsp is replaced by a manual META builder that fragments at AVRCP layer (i.e., the TG actually sets packet_type=01). Until that bypass lands, the re-emitter is dead code. Cheaper INVALID_PARAMETER (status 0x05 per AVRCP 1.3 §5.7.1 Table 5.41) reject hits a binary-shape barrier: libextavrcp.so exposes no META REJECTED builder, and changing the existing cmd_frame_ind_rsp arg shape carries regression risk for every unhandled-PDU path with no observable wire-shape benefit (CT can't tell the two reject codes apart).

Plan when revisited. Replace T4's _send_get_element_attributes_rsp call with a manual META builder that paginates at AVRCP layer (set packet_type=01 on first fragment, cache the rest in a per-conn state struct in the trampoline blob region). Upgrade T_continuation: on 0x40 emit next chunk with packet_type=02/03; on 0x41 drop state. ~3 days; needs AVRCP_SendMessage (libextavrcp.so:0x18ec) IPC shape disassembly.

9.2 AVRCP playback state ↔ AVDTP source state coupling — A2DP / AVDTP — SHIPPED

Spec. AVDTP 1.3 §8.14 / §8.15: when AVRCP TG signals PAUSED, the A2DP source should keep the stream paused (NOT torn down); resume without renegotiation on PLAYING. SUSPEND is reserved for explicit policy changes (phone call routing, etc.), not for normal pause / silence handling.

Stock deviation. AudioFlinger's silence-timeout (~3 s after the music app stops writing samples) hits libaudio.a2dp.default.so::A2dpAudioStreamOut::standby_l, which calls a2dp_stop unconditionally → AVDTP SUSPEND on the wire. Peer CTs that aggressively close + reopen their A2DP sink on SUSPEND cycle once per pause-of-≥3 s, producing burst-on-resume audio and playhead drift.

Current state. patch_libaudio_a2dp.py patches standby_l at file offset 0x000086ab (1 byte: ARM cond 0x0a EQ → 0xea AL). The beq that conditionally branched past a2dp_stop is now an unconditional b, making the call site unreachable. AudioFlinger's silence-timeout still completes the standby (releases the wake lock, sets mStandby = 1), but the AVDTP stream stays alive; the next write() after PLAYING resumes pushes samples into the same session.

Verification. Capture btlog around a pause: zero [A2DP] a2dp_stop. is_streaming:1 lines, peer A2DP sink doesn't cycle, audio resumes clean.

Sibling-path note (close_l → a2dp_cleanup). libaudio.a2dp.default.so has one other teardown PLT call: bl sym.imp.a2dp_cleanup at file 0x8a00 inside A2dpAudioStreamOut::close_l (0x89cc). close_l is reached from close() (0x8a4c) and setBluetoothEnabled(false) (0x8b58). AH1 doesn't cover this path because AudioFlinger doesn't call close() during normal pause cycles, only on policy / focus / device-routing transitions. If a future capture surfaces close_l firing during a pause, the corresponding fix is a sibling byte-flip at file 0x89e0 (beq 0x8a0c EQ → AL, mirroring AH1's shape).

9.3 Per-attribute 511-byte hard cap in …send_get_element_attributes_rsp — AVRCP — SHIPPED

Spec. AVRCP 1.3 §5.3.1 Table 5.24 places no per-attribute byte cap. TG fragments via §5.5 if the total response exceeds AVCTP MTU.

OEM TG deviation. libextavrcp.so:0x2188 (btmtk_avrcp_send_get_element_attributes_rsp) enforces a 511-byte per-attribute hard cap; on overflow it emits [BT][AVRCP][ERR] too large attr_index:%d and drops the attribute silently.

Current state. The music app's TrackInfoWriter.putUtf8Padded caps each string attribute at 240 bytes before it lands in y1-track-info. The cap is well below the OEM 511 hard cap so even after multi-byte UTF-8 expansion at the CT side the attribute survives. Truncation is codepoint-safe — if the chosen byte position would split a multi-byte codepoint, the helper walks back to the codepoint boundary so a strict CT never sees a malformed UTF-8 sequence (AVRCP 1.3 §5.3.1 Table 5.24 CharacterSet=0x6A). Numeric attributes (TrackNumber 4, TotalNumber 5, PlayingTime 7) are bounded to a 15-byte ASCII slot and unaffected.

Optional follow-up. Bypass …send_get_element_attributes_rsp entirely and build the GetElementAttributes response wire frame directly in the trampoline, using the full §5.5 fragmentation flow from §9.1 above. Removes the 511 cap as a constraint. ~3 days if paired with §9.1 (no value as a standalone change — 240 B already fits everything we'd realistically ship).

9.4 AVCTP MTU negotiation discoverability — AVCTP — NO ACTION

Spec. AVCTP 1.2 §5: each side independently advertises its MTU during L2CAP CONFIG; the smaller value wins. AVRCP 1.3 §6 specifies the AVCTP version pairing only.

Current state. mtkbt advertises AVRCP_MAX_PACKET_LEN:512 regardless of peer capability. Captured logs show (10+u2MtuPayload) <= 512 enforcement. Most peers agree to 512; some negotiate higher in L2CAP CONFIG but we cap at 512.

Compliance plan. Not a deviation worth fixing — 512 is the standard L2CAP signaling MTU. Filed here for completeness; revisit if a CT ever requests >512 and degrades on the cap.

9.5 GAVDP / AVDTP codec advertisement — GAVDP / AVDTP / A2DP — RESOLVED (closed by §9.11)

Spec. AVDTP 1.3 §8.6 (DISCOVER): TG should respond with all locally-supported SEPs. A2DP 1.3 §4.2 Table 4.1: SBC Mandatory; AAC / MP3 / ATRAC Optional.

Resolution. SBC-only is fully spec-compliant for an A2DP 1.3 Source — every non-SBC codec is Optional. No spec obligation to add codec coverage. See §9.11 for the A2DP 1.3 audit.

9.6 AVCTP subscribe-storm mitigation (U1) — AVCTP — SHIPPED

U1 (libextavrcp_jni.so:0x74e8 NOPs UI_SET_EVBIT(EV_REP)) defangs kernel-side auto-repeat on the AVRCP virtual keyboard, preventing held-key cascades when a subscribe-storming CT drops PASSTHROUGH RELEASEs. Risk-register context in §7. Patch H″ (in com.innioasis.y1.apk) handles the framework-side equivalent (InputDispatcher::synthesizeKeyRepeatLocked repeats).

9.7 SDP AVCTP version correction (V2) — AVCTP — SHIPPED

V2 (mtkbt 0x0eba6d: SDP AVCTP version 0x00 → 0x02) corrects the served record from advertising AVCTP 1.0 to AVCTP 1.2, the version paired with AVRCP 1.3 per AVRCP 1.3 §6 + ESR07 §2.1 / Erratum 4969. Byte detail in PATCHES.md.

9.8 AVDTP DELAY_REPORT (sig_id 0x0d) — AVDTP — RESOLVED (closed by §9.12)

Spec. AVDTP 1.3 §8.19 Delay Reporting. AVDTP 1.3 ICS Table 14 row 6 = Optional for the Source role.

Resolution. Optional at AVDTP 1.3 — mtkbt's lack of a 0x0d handler is spec-permissible as long as we don't advertise the Delay Reporting service capability in our SEP capabilities response (we don't). Inbound 0x0d hits the General Reject path (Mandatory per §8.18, present in mtkbt). See §9.12 for the AVDTP 1.3 audit.

9.9 Remaining workstream order

§9.2 / §9.3 / §9.5 / §9.6 / §9.7 / §9.8 shipped or resolved-no-action. Active queue:

9.10 AVCTP 1.2 ICS audit — AVCTP — VERIFIED

Anchored against docs/spec/AVCTP 1.2/AVCTP.ICS.p10.pdf Table 3 (Target Features). Per Table 0, AVCTP 1.0 / 1.2 / 1.3 are all deprecated 2022-02-01 + withdrawn 2023-02-01; only AVCTP 1.4 is in current SIG qualification scope. We target 1.2 because that's the version paired with AVRCP 1.3 per AVRCP 1.3 §6 + ESR07 §2.1 / Erratum 4969 — same shipping-window argument as AVRCP 1.3 (deprecated 2023-02-01 / withdrawn 2027-02-01).

ICS Table 3 rowCapabilityStatusY1
1Message fragmentationO✓ — mtkbt outbound path implements pkt_type 0/1/2/3 (SINGLE/START/CONTINUE/END) per ARCHITECTURE.md §"AVCTP packet types"
2Transaction label managementM✓ — transId routed from conn[17] into every response builder; stamping into byte 5 of IPC frame is the response-builder's responsibility (cross-ref ARCHITECTURE.md cross-profile coupling table item 4)
3Packet type field managementM✓ — pkt_type written by the AVCTP-layer fragmentation logic (SINGLE for the GetElementAttributes 644 B body; fragmentation untriggered at our advertised attribute sizes)
4Message type field managementM✓ — RESPONSE / COMMAND distinction set per AV/C body type; mtkbt's response builders write the correct type bit
5PID field managementM✓ — AVRCP TG PID=0x110E served correctly per the AVRCP record's ProfileDescList; AVCTP layer uses this PID for routing
6IPID field managementM✓ — Invalid Profile Identifier handling delegated to mtkbt's AVCTP layer
7Message information managementM✓ — AV/C body parsing + response synthesis lives in the trampoline chain (see ARCHITECTURE.md §"Trampoline chain")
8-13Event registration / connect / disconnect / send variantsOmtkbt provides the connect / disconnect / send paths used by the JNI bridge; event-registration optional and not separately exposed
14Multiple AVCTP channel establishmentO — Controller-only rown/a (TG row table doesn't include this)

All Mandatory rows covered. Optional row 1 (fragmentation) shipped for completeness; remaining Optional rows (8-13) are CT/TG event-registration APIs not exercised by our stack.

Static AVCTP version-byte multiplicity. Three AVCTP version sites in mtkbt's static SDP-record region; V2 patches the only one consulted at runtime. Per the SDP attribute table at vaddr 0xfa700, only one served AVRCP TG record references AVCTP — the served-record entries (file offset 0xf97c0..0xf9808) include attr 0x0004 ProtocolDescriptorList (containing the V2-patched site at 0xeba6d) but not attr 0x000d AdditionalProtocolDescriptorList. The unpatched sites 0xeba25 and 0xeba37 are dead bytes in unselected template records.

9.11 A2DP 1.3 ICS audit — A2DP — SHIPPED (PARTIAL — see §9.5 / §9.8)

Anchored against docs/spec/A2DP 1.3/A2DP_SPEC_V13.pdf. There is no separate A2DP ICS proforma in the SIG release; conformance requirements live in the spec body itself.

Application Layer (§3 Table 3.1):

ItemFeatureSRCSNKY1
1Audio StreamingMM✓ Y1 implements SRC role; SBC stream over AVDTP

Audio Codec Interoperability (§4.2 Table 4.1):

CodecStatusY1
SBCM✓ encoder present (libmtkbtextadpa2dp.so exports sbc_pack_frame, sbc_proto_4_40_fx, sbc_proto_8_80_fx, sbc_calculate_bits, etc.)
MPEG-1,2 AudioOnot advertised, not present
MPEG-2,4 AACOnot advertised, not present
ATRAC familyOnot advertised, not present

GAVDP layer rejects non-SBC SEPs at GavdpAvdtpEventCallback per ARCHITECTURE.md §"Codec scope". Spec-permissible (§4.2.4 codec interop is conditional on Optional codec support, none claimed).

SDP Source Service Record (§5.3 Figure 5.1):

AttributeSpec statusSpec value (1.3)Y1 served valueGap
Service Class ID List → Audio Source (UUID 0x110A)MUUID✓ (sdptool confirms handle 0x10002 advertises Audio Source)—
ProtocolDescriptorList → L2CAP / AVDTP, AVDTP versionM0x01030x0100 (AVDTP 1.0)1 byte at file 0xeba09
BluetoothProfileDescriptorList → AdvancedAudioDistribution (UUID 0x110D), versionM0x01030x0100 (A2DP 1.0)1 byte at file 0xeb9f2
SupportedFeatures (Source bitmap: bit 0 Player / 1 Microphone / 2 Tuner / 3 Mixer)Obit 0 = 1 (Player)not advertised (stock A2DP record omits attr 0x0311)none required (Optional)
Provider Name / Service NameOtext"Advanced Audio" served as Service Name (sdptool confirms)—

Gaps to A2DP 1.3 conformance:

  1. Advertised AVDTP version in A2DP record's ProtoDescList is 0x0100 instead of spec-mandated 0x0103 — file offset 0xeba09, single-byte patch.
  2. Advertised A2DP version in BluetoothProfileDescriptorList is 0x0100 instead of spec-mandated 0x0103 — file offset 0xeb9f2, single-byte patch.
  3. Both must move together — asymmetric advertisement is invalid. The bumps imply AVDTP 1.3 feature support (DELAY_REPORT per A2DP 1.3 §1.4.1.2; GET_ALL_CAPABILITIES per GAVDP 1.3 Table 5 row 9 once A2DP 1.3 §1.2 inherits it).

A2DP §3.1: peers consult advertised AVDTP version before GAVDP_ConnectionEstablishment. Per §9.12 AVDTP-1.3 features are Optional at the AVDTP layer; per §9.13 GAVDP 1.3 raises GET_ALL_CAPABILITIES to Mandatory at the Acceptor layer.

Shipped: V3 + V4 + V5. V3/V4 bump A2DP/AVDTP advertisement to 1.3. V5 is a 2-byte TBH jump-table alias routing sig 0x0c through the sig 0x02 GET_CAPABILITIES handler — per AVDTP 1.3 §8.8, the sig 0x02 response is a wire-compatible subset of the sig 0x0c response, sufficient for an SBC-only Source.

9.12 AVDTP 1.3 ICS audit — AVDTP — VERIFIED, GAP = ADVERTISED VERSION

Anchored against docs/spec/AVDTP 1.3/AVDTP.ICS.p14.pdf. Per Table 14a (Versions, Source), AVDTP 1.0 / 1.2 are deprecated/withdrawn; AVDTP 1.3 is the only currently-Mandatory version. Same shipping-window argument as AVRCP / AVCTP.

Source role capabilities (Table 14):

ItemCapabilityStatusY1
1Basic transport service supportM✓ — A2DP audio streams over AVDTP through libmtka2dp.so ↔ mtkbt internal A2DP source state machine ↔ AVDTP MEDIA on the wire
2Reporting service supportOnot advertised in SEP capabilities, no handler
3Recovery service supportOnot advertised, no handler
4Multiplexing service supportOnot advertised, no handler
5Robust header compression service supportOnot advertised, no handler
6Delay ReportingOnot advertised in SEP capabilities; sig_id 0x0d DELAYREPORT reaches General Reject (§8.18 fallback) — see "Handler verification" below

Acceptor capabilities (Source role) — Table 8 Signaling Message Format:

ItemCapabilityStatusY1
1Transaction LabelM✓
2Packet typeM✓
3Message typeM✓
4Signal identifierM✓

Tables 9-12 (Stream Discovery, Establishment, Suspension, Release, Security) — every row Optional. We implement Stream Discover (DISCOVER), Stream Get Capabilities (GET_CAPABILITIES), Set Configuration, Get Configuration, Open Stream, Start Stream, Suspend, Close, Abort responses (per ARCHITECTURE.md §"AVDTP signal codes" — sig 0x01..0x0b documented in mtkbt strings).

Table 13 (Message Fragmentation) item 1 = M: ✓ (mtkbt's AVDTP layer implements fragmentation, same code path as AVCTP layer per §8.3).

Table 16 (Message Error Handling): Reporting Capability Error (M) ✓ via [AVDTP_EVENT_GET_CAP_CNF]try another SEP in GavdpAvdtpEventCallback; General Reject Response Includes Signal ID (M) ✓ via §8.18 fallback path.

Handler verification for AVDTP 1.3 additions (sig_id 0x0c GET_ALL_CAPABILITIES + 0x0d DELAYREPORT):

The AVDTP signal dispatcher at file 0xaa72c (full disassembly in INVESTIGATION.md) has a TBH jump table at 0xaa81e with explicit entries for both sig 0x0c (target 0xab4de) and sig 0x0d (target 0xab540). Sig 0x0d DELAYREPORT has substantive handler logic. Sig 0x0c GET_ALL_CAPABILITIES is a stub at 0xab4de that always falls through to the BAD_LENGTH error path — peer receives an error response, not a General Reject.

V5 patches the jump-table entry for sig 0x0c to redirect to the sig 0x02 GET_CAPABILITIES handler at 0xaa924. This is a structural workaround, not a real handler — but per AVDTP 1.3 §8.8 the sig 0x02 response is a wire-compatible subset of the sig 0x0c response, which suffices for an SBC-only Source.

Compliance verdict.

AspectStatus
All Mandatory rows of Tables 8 / 13 / 16 (Source-role Acceptor) covered✓
All advertised SEP capabilities backed by working handlers (SBC + Basic transport service)✓
AVDTP 1.3 Optional features (Delay Reporting, Reporting / Recovery / Multiplexing services)not advertised, not implemented — spec-permissible
Advertised AVDTP version in SDP record0x0100 (AVDTP 1.0) — gap; spec target 0x0103

Gap: the static AVDTP version byte at file offset 0xeba09 is 0x00 instead of 0x03. Standalone fix would be a single-byte patch (V4-style: 0x00 → 0x03). However, the bump implies A2DP 1.3 conformance (peers consult our AVDTP version per A2DP §3.1), which transitively implies GAVDP 1.3 conformance (per A2DP 1.3 §1.2 profile dependency), which has a strict-conformance gap on sig 0x0c GET_ALL_CAPABILITIES — see §9.13 for the full picture. Bump decision deferred to §9.11 user-approval point.

This partially resolves the §9.5 / §9.8 open investigations — the codec-advertisement question is closed (SBC-only at A2DP 1.3 is spec-permissible since AAC/MP3/ATRAC are Optional). The DELAY_REPORT question is closed at the AVDTP-1.3-Optional-feature level (we don't advertise it in our SEP capabilities, so the missing handler is not a deviation). The GET_ALL_CAPABILITIES question survives and feeds the §9.13 GAVDP-layer Mandatory-row analysis.

9.13 GAVDP 1.3 ICS audit — GAVDP — VERIFIED, GAP = SIG 0x0C HANDLER

Anchored against docs/spec/GAVDP 1.3/GAVDP.ICS.p12.pdf. Per Tables 2a / 3a, GAVDP 1.0 / 1.2 are deprecated/withdrawn; GAVDP 1.3 is the only currently-Mandatory version. GAVDP isn't separately advertised in any SDP record (UUID 0x1203 hits in mtkbt's static SDP region are part of the HFP / HSP records, not GAVDP); A2DP 1.3 §1.2 makes A2DP-1.3 conformance imply GAVDP-1.3 conformance.

Acceptor capabilities (Table 3 — our role since peers initiate):

ItemFeatureStatusY1
1Connection EstablishmentM✓ — peer-initiated AVDTP connection accepted
2Transfer Control – SuspendOresponse handled per btmtk_a2dp_send_stream_pause_req (_handle_stream_suspend_ind/_cnf)
3Transfer Control – Change ParametersORECONFIGURE response present in mtkbt sig table
4Start StreamingM✓ — _handle_stream_start_ind/_cnf
5Connection ReleaseM✓ — _handle_stream_close_ind/_cnf
6Signaling Control – AbortM✓ — _handle_stream_abort_ind/_cnf
7Security ControlOnot implemented; peers don't issue Content Security Control commands in our test matrix
8Delay ReportingOnot implemented (matches §9.12)

Acceptor AVDTP procedures (Table 5):

ItemFeatureStatusY1
1Stream discover responseM✓ — DISCOVER (sig 0x01)
2Stream get capabilities responseM✓ — GET_CAPABILITIES (sig 0x02)
3Set configuration responseM✓ — SET_CONFIGURATION (sig 0x03)
4Open stream responseM✓ — OPEN (sig 0x06)
5Start stream responseM✓ — START (sig 0x07)
6Close stream responseM✓ — CLOSE (sig 0x08)
7General reject messageM✓ — §8.18 fallback path
8Abort stream responseM✓ — ABORT (sig 0x0a)
9Stream get all capabilities responseM✗ — sig 0x0c hits General Reject; no real handler

Item 9 is the gap. AVDTP 1.3 ICS Table 10 row 6 says it's Optional at the AVDTP layer; GAVDP 1.3 ICS Table 5 row 9 raises it to Mandatory at the GAVDP-Acceptor layer with Inter-Layer Dependency AVDTP 10/6 OR AVDTP 10b/6 (Source row OR Sink row).

Interop reality vs ICS-strict reading. Stock without V5: a peer that issues sig 0x0c receives an AVDTP General Reject per §8.18 (Mandatory, present). Well-behaved peers fall back to plain sig 0x02 GET_CAPABILITIES, which we honor. The ICS gap is paper-only at the conformance level, not a functional interop break.

Verdict. GAVDP 1.3 conformance is strict-conformant on every Mandatory row except Acceptor Table 5 row 9 (sig 0x0c GET_ALL_CAPABILITIES response). The gap survives whether or not we bump §9.11 / §9.12 advertised versions — the only difference is whether we claim GAVDP 1.3 (by virtue of A2DP 1.3 dependency, which inherits AVDTP 1.3 dependency). At current 1.0 advertisement, we don't claim GAVDP 1.3 conformance; the gap exists but isn't load-bearing.

Implementation cost for a real 0x0c handler: disassembly of mtkbt's existing GET_CAPABILITIES (sig 0x02) response builder + extension to emit the Get All Capabilities response shape (GET_CAPABILITIES response + Reporting Service / Recovery Service / Multiplexing Service / Robust Header Compression / Delay Reporting service capabilities, all advertised as "not supported"). Not currently in scope.

9.14 Lower-BT-stack gap analysis — synthesis

Combining §9.10 (AVCTP) + §9.11 (A2DP) + §9.12 (AVDTP) + §9.13 (GAVDP):

ProfileStock advertisesSpec targetM-row gapsPatch neededStatus
AVRCP1.01.3noneV1 (0xeba58 0x00→0x03)✓ shipped
AVCTP1.0 (signaling)1.2 (paired with AVRCP 1.3)noneV2 (0xeba6d 0x00→0x02)✓ shipped
A2DP1.01.3 (era-correct)inherits GAVDP gap on sig 0x0c (closed by V5)V3 (0xeb9f2 0x00→0x03)✓ shipped
AVDTP1.01.3 (paired with A2DP 1.3)none at AVDTP layer; sig 0x0c is Optional hereV4 (0xeba09 0x00→0x03)✓ shipped
GAVDPnot separately advertised; piggybacks AVDTP version1.3 (transitive)Table 5 row 9 (sig 0x0c GET_ALL_CAPABILITIES response, Mandatory at GAVDP-Acceptor)V5 (TBH jump-table alias at 0xaa834)✓ shipped

Status: Option C shipped — V3 + V4 + V5 landed together. V3 + V4 bump A2DP/AVDTP advertisement to 1.3; V5 is the structural workaround for sig 0x0c — it aliases the dispatcher's TBH jump-table entry from the BAD_LENGTH stub at 0xab4de to the existing GET_CAPABILITIES handler at 0xaa924. Per AVDTP 1.3 §8.8 the sig 0x02 response is a wire-compatible subset of the sig 0x0c response (no extended Service Capabilities), which is exactly what our SBC-only Source advertises anyway.

V5 is wire-correct by decoupling. The AVDTP wire-frame TX site is fcn.000ae418 (calls L2CAP_SendData at file offset 0xae58e). Byte 1 of the response frame (sig_id) is read at 0xae480 from txn->[0xe], where txn = *(channel + 0x10) is the per-channel transaction state populated by the request parser at signal-RX time. The dispatcher (TBH at 0xaa81e) and per-signal handlers (e.g. 0xaa924) do not write txn->[0xe]. When a peer issues sig 0x0c the parser stores 0x0c in txn->[0xe], V5 routes dispatch through the GET_CAPABILITIES handler, the handler updates state, and fcn.000ae418 reads txn->[0xe]=0x0c and emits a response with sig_id=0x0c. Full walk-through in INVESTIGATION.md.

patch_mtkbt.py OUTPUT_MD5 reflects V1+V2+V3+V4+V5+S1+P1.


10. See also

Published by the ryan-specter/koensayr-auto project [1] [3]