docs(openspec): archive bt-device-identity and promote its spec
Move the change folder to changes/archive/2026-07-11-bt-device-identity with the verified artifact set (verdict: pass with warnings, 0 critical, 102/102 targeted tests) and create the bt-device-identity capability spec under openspec/specs/. Phase 7 on-device QA remains the pending human gate before release.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# Spec: Canonical Bluetooth Device Identity
|
||||
|
||||
## New Capability: bt-device-identity
|
||||
|
||||
### Purpose
|
||||
|
||||
Ensures each physical Bluetooth A2DP device produces a stable, unique `deviceId` and displays its own Bluetooth name by default, by acquiring `BLUETOOTH_CONNECT` at point-of-intent, guarding against the OS placeholder MAC, caching per-device platform names, and migrating away corrupted placeholder-keyed persistence entries.
|
||||
|
||||
### Requirements
|
||||
|
||||
#### Requirement: BLUETOOTH_CONNECT requested at point-of-intent
|
||||
|
||||
On API 31+, the system MUST request `BLUETOOTH_CONNECT` when the device-management UI (Settings → Advanced Equalization) is opened, not at app launch. `AndroidManifest.xml` MUST declare `BLUETOOTH_CONNECT`.
|
||||
|
||||
##### Scenario: permission requested on UI open — manual/on-device QA
|
||||
|
||||
- GIVEN the app has never requested `BLUETOOTH_CONNECT`
|
||||
- WHEN the user opens the device-management screen on API 31+
|
||||
- THEN the system permission dialog MUST appear before any BT device is enumerated for identity purposes
|
||||
|
||||
##### Scenario: permission not requested at app launch — manual/on-device QA
|
||||
|
||||
- GIVEN a fresh install
|
||||
- WHEN the app starts and the user never opens device-management
|
||||
- THEN no BT permission dialog MUST appear
|
||||
|
||||
---
|
||||
|
||||
#### Requirement: Placeholder MAC guarded with composite fallback
|
||||
|
||||
`deviceToMap()` MUST treat the OS placeholder `"02:00:00:00:00:00"` as absent, not as a usable MAC. When the MAC is absent or the placeholder, the id MUST fall back to a deterministic composite id keeping the single leading `bt_a2dp:` segment, with the product name colon-sanitized (implemented shape: `"bt_a2dp:name:<sanitized productName>"`, per design ADR-3 — the fixed `name` marker keeps matrix-key split-on-first-colon safety); the placeholder string itself MUST NOT become a `deviceId`.
|
||||
|
||||
##### Scenario: permission granted yields real MAC id — Dart-testable via fake device stream
|
||||
|
||||
- GIVEN `BLUETOOTH_CONNECT` is granted
|
||||
- WHEN a BT A2DP device with MAC `AA:BB:CC:DD:EE:FF` connects
|
||||
- THEN `deviceId` MUST be `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
|
||||
|
||||
##### Scenario: permission granted yields real MAC id — manual/on-device QA (Kotlin `deviceToMap`)
|
||||
|
||||
- GIVEN `BLUETOOTH_CONNECT` is granted on a real device
|
||||
- WHEN `deviceToMap()` runs for a connected BT A2DP device
|
||||
- THEN `device.address` MUST be a real MAC, not the placeholder
|
||||
- AND the resulting id MUST NOT equal `"bt_a2dp:02:00:00:00:00:00"`
|
||||
|
||||
##### Scenario: permission denied yields composite fallback, no crash — Dart-testable via fake device stream
|
||||
|
||||
- GIVEN `BLUETOOTH_CONNECT` is denied or `device.address` reports the placeholder
|
||||
- WHEN a BT A2DP device with `productName` "AirPods Pro" connects
|
||||
- THEN `deviceId` MUST be a deterministic composite id (implemented shape: `"bt_a2dp:name:AirPods Pro"`), MUST NOT be `"bt_a2dp:02:00:00:00:00:00"`, and app state MUST update without throwing
|
||||
|
||||
##### Scenario: multiple denied-permission devices do not collide — Dart-testable via fake device stream
|
||||
|
||||
- GIVEN `BLUETOOTH_CONNECT` is denied
|
||||
- WHEN two distinct BT A2DP devices with different `productName` values connect in sequence
|
||||
- THEN each MUST produce a distinct composite `deviceId`
|
||||
- AND `presetsDispositivo` MUST NOT collapse them into one entry
|
||||
|
||||
##### Scenario: placeholder never surfaces as a stored id — Dart-testable
|
||||
|
||||
- GIVEN any device change event is processed after the fix
|
||||
- WHEN persistence keys are inspected
|
||||
- THEN no key MUST equal the literal string `"bt_a2dp:02:00:00:00:00:00"`
|
||||
|
||||
---
|
||||
|
||||
#### Requirement: Per-device platform-name cache feeds display
|
||||
|
||||
`EstadoEcualizador` MUST cache the last-seen platform (`productName`) name per `deviceId`. `nombreVisible(deviceId, platformName)` call sites in `pantalla_ajustes.dart` MUST pass the cached platform name, not an empty string.
|
||||
|
||||
##### Scenario: platform name displays with no custom rename — Dart-testable
|
||||
|
||||
- GIVEN a device with id `"bt_a2dp:AA:BB:CC:DD:EE:FF"` and platform name "AirPods Pro" has connected, with no custom rename stored
|
||||
- WHEN the device row is rendered
|
||||
- THEN the displayed name MUST be "AirPods Pro", not the raw `deviceId`
|
||||
|
||||
##### Scenario: custom rename overrides platform name — Dart-testable
|
||||
|
||||
- GIVEN the same device has both a cached platform name "AirPods Pro" and a custom rename "My Headphones"
|
||||
- WHEN the device row is rendered
|
||||
- THEN the displayed name MUST be "My Headphones"
|
||||
|
||||
##### Scenario: no platform name yet falls back to raw id — Dart-testable
|
||||
|
||||
- GIVEN a `deviceId` has no cached platform name and no custom rename
|
||||
- WHEN the device row is rendered
|
||||
- THEN the displayed name MUST be the raw `deviceId` (unchanged legacy behavior)
|
||||
|
||||
---
|
||||
|
||||
#### Requirement: Rename overlay survives re-pair under canonical id
|
||||
|
||||
Once `BLUETOOTH_CONNECT` is granted, a rename stored under a device's canonical MAC-based `deviceId` MUST persist across disconnect/reconnect (re-pair) of the same physical device.
|
||||
|
||||
##### Scenario: rename persists after re-pair — Dart-testable via fake device stream
|
||||
|
||||
- GIVEN a custom rename "My Headphones" is stored for `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
|
||||
- WHEN that device disconnects and reconnects, reporting the same MAC
|
||||
- THEN the device row MUST still display "My Headphones"
|
||||
- AND no second/duplicate entry MUST appear in `presetsDispositivo`
|
||||
|
||||
##### Scenario: rename persists after re-pair — manual/on-device QA
|
||||
|
||||
- GIVEN a real paired BT device is renamed in-app
|
||||
- WHEN the user disconnects and re-pairs the same physical device
|
||||
- THEN the rename MUST still be shown and no duplicate device row MUST appear
|
||||
|
||||
---
|
||||
|
||||
#### Requirement: `_onDispositivoCambiado` does not create duplicate entries for transient reports
|
||||
|
||||
Device-change events reporting an id already present in `presetsDispositivo` MUST NOT create a second entry or overwrite the existing preset with a fresh copy.
|
||||
|
||||
##### Scenario: repeated event for known id is a no-op on preset creation — Dart-testable
|
||||
|
||||
- GIVEN `presetsDispositivo` already contains an entry for `deviceId`
|
||||
- WHEN `_onDispositivoCambiado` fires again for the same `deviceId`
|
||||
- THEN `presetsDispositivo[deviceId]` MUST remain unchanged
|
||||
- AND no new key MUST be added to `presetsDispositivo`
|
||||
|
||||
##### Scenario: transient non-BT id during pairing handshake does not corrupt BT entry — Dart-testable via fake device stream
|
||||
|
||||
- GIVEN a BT device is mid-reconnect and the OS transiently reports `"builtin_speaker"` before A2DP profile attaches
|
||||
- WHEN both the transient and final BT events are processed
|
||||
- THEN the BT device's own entry MUST be keyed only by its BT `deviceId`
|
||||
- AND MUST NOT be merged with or overwritten by the transient `"builtin_speaker"` entry
|
||||
|
||||
---
|
||||
|
||||
#### Requirement: One-time guarded migration purges only exact placeholder-keyed entries
|
||||
|
||||
On first load after this change, the system MUST remove entries whose key is exactly `"bt_a2dp:02:00:00:00:00:00"` from `eq_nombres_dispositivos_v1`, `eq_presets_matriz_v1` (matching the `deviceId` segment), and `eq_preset_por_dispositivo_v1`. All other entries MUST be preserved unchanged. The migration MUST run at most once (idempotent, flagged).
|
||||
|
||||
##### Scenario: migration removes only exact placeholder entries — Dart-testable
|
||||
|
||||
- GIVEN `eq_preset_por_dispositivo_v1` contains both `"bt_a2dp:02:00:00:00:00:00"` and `"bt_a2dp:AA:BB:CC:DD:EE:FF"`
|
||||
- WHEN the migration runs
|
||||
- THEN the placeholder-keyed entry MUST be removed
|
||||
- AND the stable-MAC entry MUST remain byte-for-byte identical
|
||||
|
||||
##### Scenario: matrix keys purge only the placeholder segment — Dart-testable
|
||||
|
||||
- GIVEN `eq_presets_matriz_v1` contains `"station1:bt_a2dp:02:00:00:00:00:00"` and `"station1:bt_a2dp:AA:BB:CC:DD:EE:FF"`
|
||||
- WHEN the migration runs
|
||||
- THEN only the entry with the placeholder `deviceId` segment MUST be removed
|
||||
- AND the other matrix entry MUST be preserved unchanged
|
||||
|
||||
##### Scenario: near-miss keys are preserved — Dart-testable
|
||||
|
||||
- GIVEN a key `"bt_a2dp:02:00:00:00:00:01"` exists (differs by one digit from the placeholder)
|
||||
- WHEN the migration runs
|
||||
- THEN this entry MUST NOT be removed
|
||||
|
||||
##### Scenario: migration runs once — Dart-testable
|
||||
|
||||
- GIVEN the migration has already run once (flag set)
|
||||
- WHEN the app loads again with the same data
|
||||
- THEN the migration MUST NOT execute a second time
|
||||
- AND no additional entries MUST be removed
|
||||
|
||||
##### Scenario: no placeholder entries when BLUETOOTH_CONNECT was never requested — Dart-testable
|
||||
|
||||
- GIVEN no BT permission was ever granted and no placeholder-keyed entries exist
|
||||
- WHEN the migration runs
|
||||
- THEN it MUST be a no-op and MUST NOT alter any persisted keys
|
||||
|
||||
---
|
||||
|
||||
### Localization
|
||||
|
||||
#### Requirement: Permission rationale and migration notice strings exist in all locales
|
||||
|
||||
If UI copy is shown for the `BLUETOOTH_CONNECT` rationale or the migration notice, new l10n keys MUST be added to `lib/l10n/app_en.arb` as the template and translated in all 13 target locales: `ar`, `bn`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `zh`.
|
||||
|
||||
##### Scenario: new keys present in every locale — Dart-testable (arb parity check)
|
||||
|
||||
- GIVEN new keys `btConnectRationale` and `eqDeviceMigrationNotice` (or equivalent) are added to `app_en.arb`
|
||||
- WHEN each of the 13 `app_<locale>.arb` files is parsed
|
||||
- THEN each file MUST contain both keys with non-empty translated values
|
||||
|
||||
##### Scenario: rationale copy renders before the OS dialog — manual/on-device QA
|
||||
|
||||
- GIVEN the device-management UI is opened for the first time
|
||||
- WHEN the in-app rationale (if any) is shown
|
||||
- THEN it MUST render in the user's selected app locale before the OS permission dialog appears
|
||||
Reference in New Issue
Block a user