dabca3ad24
android / Build + lint + test (push) Successful in 3m44s
When the phone was locked, a transport command (lock-screen/Bluetooth/ watch media button, or queue-resync play) issued during a WiFi power-save stall would hit the 2s connect timeout and throw. handleSoapFailure then cancelled the poll loop and fired onDrop on that single failure -- showing "Disconnected from <Sonos>" and reverting to local playback, even though the renderer was perfectly reachable. This bypassed the poll loop's deliberate 30-consecutive-failure tolerance (DROP_THRESHOLD, bumped from 3 precisely for screen-off WiFi sleep / Doze). Make the 1 Hz poll loop the sole drop arbiter: - Transport SOAP commands retry transient IO failures (retryTransientIo: 3 attempts, 400ms backoff) so a brief WiFi stall lands the command once WiFi wakes instead of abandoning it. A SoapFaultException (renderer answered, rejected the action) is not retried -- the device is alive. - handleTransportFailure no longer cancels the poll loop or fires onDrop; it logs and nudges an immediate poll so the UI reconciles to Sonos's actual state. If the renderer is truly gone, the poll loop trips the drop on its own via DROP_THRESHOLD. Extract retryTransientIo as an internal top-level fn + unit test covering first-success, retry-then-succeed, exhaust-and-rethrow, and no-retry-on- SoapFault. Refresh now-stale drop-heuristic comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>