Decides every radio candidate raised by the 2026-08-28 community baseline (CR-020). The
comparison is against what SaicRadio, SaicMediaPlayer and ActionType already do, not
against the initial community audit, which under-counted the existing catalogue.
Provenance. CarMediaPlayer (MIT) and the community DAB+ fix (no explicit license at the
baseline review, and it replaces an OEM application) are read as behaviour only. No code,
resource or replacement strategy from either is used here. What the head unit's own radio
app does is the evidence that matters, and the binding contract below comes from the vendor
SDK already documented in SaicMedia.kt.
| Capability | Where | Reachable from a rule |
|---|---|---|
Play / resume the tuner (srcPlayRadio, tx 27) |
SaicRadio.play |
PLAY_RADIO |
| Tune FM/AM by frequency (tx 8) | SaicRadio.tune |
TUNE_RADIO |
| Next / previous station (tx 13 / 14) | SaicRadio.nextStation / previousStation |
RADIO_NEXT_STATION, RADIO_PREV_STATION |
Read the tuner's play state (tx 19, RadioBean.state) |
SaicRadio.isPlaying |
— (internal) |
Pause / silence the tuner (srcPauseRadio, tx 28) |
SaicRadio.pause |
— (internal) |
| Open the radio screen (tx 26) | SaicRadio.openScreen |
— (internal) |
| Play/pause of whatever owns the audio | SaicMediaPlayer.command |
MEDIA_CONTROL |
Three primitives existed in the library with no way to ask for them from a rule. That, not a missing service call, is the actual gap.
| # | Candidate | Decision | Why |
|---|---|---|---|
| A | Pause / stop the radio | add — PAUSE_RADIO |
srcPauseRadio is implemented and unreachable. MEDIA_CONTROL is not a substitute: it addresses whichever source owns the audio, so it silences Bluetooth when Bluetooth is playing. "Silence the tuner" was not expressible. |
| B | Explicit radio play/pause toggle on readable state | add — RADIO_PLAY_PAUSE |
One shortcut for a driver who wants the tuner specifically, whatever the current source. Built on isPlaying() (RadioBean.state), because AudioManager.isMusicActive is false while the radio plays — a toggle driven by it would send "play" to a playing radio forever. Unknown state fails closed (see below). |
| C | Open the radio screen | add, gated — OPEN_RADIO_SCREEN |
startRadioActivity is implemented and unreachable. It is the one radio action that is not audio-only: throwing a full-screen app in front of a driver at speed is a distraction, so it takes the standstill gate and is refused when speed is unreadable. |
| D | Select a band | added (2026-08-29, reversed), merged into TUNE_RADIO (2026-09-03) |
Originally rejected because the binding contract exposes no band-only call. The owner asked for DAB support anyway, and the rejection turned out to be answering the wrong question: a band action does not need a band-only call, because tune(band, frequencyKhz) already carries the band as its first argument. SELECT_RADIO_BAND composes what is established — read the current band from RadioBean, tune into the requested one — and guesses no transaction code. The frequency it lands on is the station this car was last heard on for that band (RadioBandMemory), or the band floor followed by one nextStation() when it has never been there, so it does not strand the driver on an empty frequency. It shipped as its own SELECT_RADIO_BAND entry and was folded into TUNE_RADIO a week later — see Merge below. |
| E | Select a DAB service | reject (unchanged) | A DAB service is addressed by ensemble id + service id, not by a frequency, and neither is a stable identifier a driver could enter or a rule could store across a re-scan. No on-vehicle evidence establishes a portable contract. DAB is now reachable as a band (candidate D, inside TUNE_RADIO) and stepped through with RADIO_NEXT_STATION / RADIO_PREV_STATION; what stays out is selecting one named service directly. A DAB block is a real frequency in Band III, which is what a band switch tunes; a DAB service is not. |
| F | Restore the last audible radio state at ignition | already covered | Composition, not a new action: an ignition trigger plus PLAY_RADIO, whose srcPlayRadio resumes the last station by itself. A persistent service that remembered and re-asserted state would be a background watchdog the app does not have and does not want. |
| G | Radio artwork caching, station metadata display | reject as an action | Presentation, not automation. A rule cannot usefully "cache artwork"; a screen can show it. If it is ever wanted it belongs to an app's UI, reading RadioBean, not to the action catalogue. |
| H | Replace or patch the OEM radio APK (the DAB+ fix's strategy) | reject | Out of scope by the ticket and by the suite's boundary: EVSuite talks to the vendor services the car ships. It does not swap the car's applications, and the upstream fix carries no license that would permit reuse anyway. |
- Missing service. All three call
SaicRadio, whoseservice.binder()is null when the radio service is absent or its bind has not landed. Every call reports false; the executor turns that into an error rather than a silent success. EVTasker's diagnostic screen answersNO_VENDOR_SERVICEfor the whole radio family — includingRADIO_NEXT_STATIONandRADIO_PREV_STATION, which were answering for the AOSP car layer instead. - Unreadable play state.
RADIO_PLAY_PAUSEreadsisPlaying()first and sends nothing when it answers null:ToggleResult.STATE_UNKNOWN, reported as an error with its reason. It does not fall back to "assume playing" the waySaicMediaPlayer.radioCommanddoes — that fallback is right for a media-key shortcut a driver can press twice, and wrong for a rule that fires unattended and would toggle the audio to the state nobody chose. - Unsupported firmware.
@SupportedOn(SWI68, SWI165)on all three, the generations where the radio service is bound. The A9 generations (SWI69, SWI131, SWI132) have no such service;ActionCompatibilityrefuses the action there before it is attempted, and the editor does not offer it.
Candidate D was reopened at the owner's request after an on-vehicle diagnostic (SWI68). The
original rejection assumed a band action required a band-only transaction, and that assumption
was wrong: tune takes the band as an argument, so the band is reachable through a call that
was already proven. The rejection of candidate E stands unchanged — a DAB service still has no
identifier stable across a re-scan, and none was guessed.
Candidate D shipped as an action of its own, and that was one entry too many. Band, frequency
and playback are three parts of one instruction — "put the radio on FM 103.5 and play it" — and
splitting them left each part unable to say the whole thing: SELECT_RADIO_BAND could not name
a station, TUNE_RADIO could not reach DAB, and a driver who wanted both wrote two rows with a
delay between them. They are now one entry, TUNE_RADIO, carrying the band in number, the
frequency in text and playback in flag.
Nothing about the vehicle contract changed: a frequency still goes through tune, a band with
no frequency still goes through selectBand, and DAB is still reachable only as a band. What
changed is which of the two a rule asks for, and that is now read off the rule rather than off
which of two entries it picked. selectBand gained andPlay so that the merged action's
"enable radio" switch means the same thing on both paths.
Two things are refused rather than resolved. A typed frequency that disagrees with the picked
band (AM 103.5 under an FM picker) parses to nothing, as AM 103.5 already did on its own —
two things were said, and silently keeping one is how a rule lands on a station nobody chose.
And DAB takes no frequency at all: the editor removes the field rather than accept a number it
would have to ignore.
Rules and exported files that still name SELECT_RADIO_BAND are rewritten on load by
EVTasker's LegacyRuleJson, keeping their band and their playback — the old band switch always
took the audio focus, so dropping playback would have silenced a rule that used to make a
sound.
(pending — CR-020 requires a run. Record, per firmware: SaicRadio.isAvailable, whether
isPlaying() answered on FM, AM and DAB, whether PAUSE_RADIO silenced the tuner, whether
RADIO_PLAY_PAUSE toggled in the direction the state predicted, whether
OPEN_RADIO_SCREEN was correctly refused while moving, and whether TUNE_RADIO reached DAB
with its frequency left empty and came back to FM on the station it had left.)
Availability is evidenced on SWI68: the 2026-08-29 diagnostic reported every radio action
[ OK ], with the radio service bound. What is still missing is the behavioural half — that
each one does what its label says.