feat(service): Companion Device Manager associations for BLE radios (Phase 1) - #6477
Draft
jamesarich wants to merge 1 commit into
Draft
feat(service): Companion Device Manager associations for BLE radios (Phase 1)#6477jamesarich wants to merge 1 commit into
jamesarich wants to merge 1 commit into
Conversation
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…Phase 1) Android 12+ refuses startForegroundService() from the background, which strands the DeviceAddressChanged trigger when a device switch completes after the app is backgrounded. A Companion Device Manager association plus REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND (API 31, exactly coextensive with the restriction) is a named exemption. - Manifest: companion FGS-start permission + optional companion_device_setup uses-feature. Deliberately no RUN_IN_BACKGROUND / USE_DATA_IN_BACKGROUND battery-leniency grants. - New CompanionAssociationRepository (core/ble androidMain): API fork (26-32 string API vs 33+ AssociationInfo), feature-guarded, chooser IntentSenders surfaced via flow and launched by MainActivity. - Pairing hook: successful bond offers an association (fire-and-forget, never gates the connect). - One-time dismissible migration prompt on the Connections screen while connected over BLE. - ForegroundStartPolicy gains hasCompanionAssociation; a backgrounded DeviceAddressChanged start becomes legal for an associated radio. The starter keeps its try/catch: the OS verifies the exemption claim. - Stale-association hygiene: an association whose device is no longer bonded (adapter ON) is disassociated — unpairing is how radios are removed; there is no in-app BLE forget flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jamesarich
force-pushed
the
feat/cdm-association-phase1
branch
from
August 1, 2026 15:24
d9efb64 to
b5cae14
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Android 12+ refuses
startForegroundService()from the background. PR #6471 made that refusal safe (the policy declines the start instead of letting the watchdog kill the process), but theDeviceAddressChangedtrigger still strands: when a device switch completes after the app is backgrounded, MeshService simply doesn't start until the user reopens the app.A Companion Device Manager association plus
REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND(API 31 — exactly coextensive with the background-start restriction, verified via api-versions.xml) is a named exemption to that restriction. This is Phase 1 of the phased CDM adoption plan: associations only. NoCompanionDeviceService, presence observation, or presence-driven teardown (Phase 2/3), and deliberately noREQUEST_COMPANION_RUN_IN_BACKGROUND/REQUEST_COMPANION_USE_DATA_IN_BACKGROUND— the foreground service makes those broad battery-leniency grants unnecessary.🌟 What changed
<uses-feature android:name="android.software.companion_device_setup" android:required="false"/>.CompanionAssociationRepository(core/ble, androidMain):hasAssociationFor/associate/disassociatewith the API fork inside (26–32 deprecated string API vs 33+AssociationInfo/disassociate(int)), every entry point guarded onFEATURE_COMPANION_DEVICE_SETUP. The CDM chooserIntentSenders surface on a flow;MainActivitylaunches them viaStartIntentSenderForResultand the association truth is always re-queried from the platform, never inferred from the result.AndroidScannerViewModelrequests an association (BluetoothDeviceFilter+setSingleDevice(true)). Fire-and-forget: user cancel or failure leaves the connect flow exactly as before — associations are strictly additive.ForegroundStartPolicy.isForegroundStartAllowedgainshasCompanionAssociation— a backgroundedDeviceAddressChangedstart becomes legal for an associated radio.MeshServiceStarterkeeps its try/catch, so the exemption claim is verified by the OS rather than trusted.removeBondhas no callers; recents are TCP-only) — radios are removed by unpairing. SohasAssociationForreconciles against the bonded set: an association whose device is no longer bonded is disassociated and no longer counts. Only positive knowledge revokes — a disabled adapter reports an empty bonded set and is ignored.Applies to google + fdroid identically (androidMain only); desktop/iOS untouched.
🧪 Testing performed
ForegroundStartPolicyTest: new parameter across trigger × SDK matrix, incl. backgroundDeviceAddressChangedallowed with association / refused without, and the background-start ↔ while-in-use invariant now checked with and without an association.CompanionAssociationRepositoryTest(Robolectric, per-test@Config(sdk = 31/32/34)): both API forks, feature-absent no-ops, chooser emission viaonDeviceFound(26–32) andonAssociationPending(33+), single-device request shape, case-insensitive MAC match, stale-association drop, adapter-off keep.AndroidScannerViewModelBondingTest: successful bond requests an association without gating the connect.spotlessApply spotlessCheck detekt assembleDebug test allTests.📲 On-device gate (before merge)
The trigger this exemption rescues is a
DeviceAddressChangedthat lands while the app is backgrounded — the user never "switches devices from the background"; the switch completes asynchronously after they leave. Two real producers:setDeviceAddressprogrammatically —SecureDfuHandler/Esp32OtaUpdateHandlerpark the address on"n"during the flash and the update flow switches back to the radio afterwards. Backgrounding the app during a multi-minute update is completely normal, and without this exemption the switch-back is refused (Skipping MeshService startin logs) and the radio stays disconnected until the app is reopened.Gate: with an associated radio, start a firmware update, background the app mid-flash, and verify the reconnect start proceeds (no
Skipping MeshService start) — on both google and fdroid flavors. (The Phase 2 PR #6479 adds the much more natural presence path — radio power-cycle while backgrounded — which exercises this same policy branch.)🤖 Generated with Claude Code