Reported from the phone's diag log:
status: crashed: SecurityException: WifiService: Neither user 10519 nor
current process has android.permission.ACCESS_WIFI_STATE.
at android.os.Parcel.createExceptionOrNull
`PhoneSensors.wifiSsid` read `WifiManager.connectionInfo` behind nothing
but the location grant, but WifiService gates every WifiManager call on
ACCESS_WIFI_STATE — and it enforces that by throwing SecurityException,
not by answering null. The permission was never declared, so the read
always threw.
The cost is larger than the missing SSID: the throw travels out of
`readSnapshot` into the worker's catch, which logs "status: crashed" and
returns failure — so one undeclared permission took the battery, network,
DND and location topics down with it, and the fleet's rows went stale
while the app looked healthy.
Two halves, both needed:
- the manifest declares ACCESS_WIFI_STATE (a normal permission: declaring
it is the whole grant, no runtime prompt). An install that predates the
line keeps the grant set it was installed with, so this alone does not
stop the crash on an existing phone;
- `wifiSsid` returns null unless the permission is actually held, and
wraps the read in `runCatching`, so the status report degrades to "no
wifi name" instead of dying whole. The check is a `hasWifiState` beside
the existing `hasLocation`, same idiom.
The stored `colors` key names a palette — rhema (the default,
Jadeite), aquamarine, black-and-white, or dracula — and the
Appearance page of /settings cycles it live, like the theme and
icon toggles beside it. Each palette carries a dark and a light
appearance under the existing `theme` key; dracula's light side is
derived, the official palette ships only dark.
The appearance decision moves off token comparison: Theme now
answers is_dark() from the ground/text luminance, so palettes
beyond the original two get the right per-profile fallback tones.