Skip to content

Fix/android alerts and location - #544

Merged
whes1015 merged 7 commits into
mainfrom
fix/android-alerts-and-location
Aug 19, 2026
Merged

Fix/android alerts and location#544
whes1015 merged 7 commits into
mainfrom
fix/android-alerts-and-location

Conversation

@whes1015

Copy link
Copy Markdown
Member

這個 PR 做了什麼

相關 issue

  • closes #

怎麼驗

檢查清單

  • tool/check/commits.sh origin/main..HEAD 通過
    —— commit 訊息就是更新日誌,格式見 commit.md
  • 一個 commit 一件事(這條 gate 驗不了,靠自己和 review)
  • mise exec -- flutter analyzemise exec -- flutter test 通過
  • 新的使用者可見字串都走 AppLocalizations,沒有寫死
  • 有 UI 變更的話:用的是 AppSpacing / AppRadius / AppMotion
    深色模式看過,文字對比度可接受

Arming the geofence cancelled the alarm. A Pixel 9 armed at 01:09 and 133
minutes later had reported nothing: `dumpsys alarm` held no pending alarm,
`geofence_armed` was true, `wake_geofence_n` was 1, and the only breadcrumb
was "arm: geofence live". A fence that registers is not a fence that fires,
and cancelling on `armed` made those two states the same observation.

So the alarm is a watchdog now and the fence is what pets it. Both proofs
count and they prove different halves: a fence that *arms* has reached Play
services, a fence that *fires* has reached us. Either restarts the hour, so
behind a working fence the alarm is perpetually pushed out and never runs,
and an hour of fence silence is what lets it through. An error broadcast
does not count — it is evidence the fence is gone, so it pulls the alarm in
instead.

Four states had no alarm at all, each of them permanent:
  - a reboot or an app update clears every elapsed-realtime alarm, and the
    boot receiver's success branch scheduled nothing behind the fence it had
    just re-armed;
  - enabling reporting waits on a fix and then on an asynchronous
    registration, and scheduled nothing for that whole window — so a
    first-ever enable, or any stop/start from the developer page, could die
    in it with reporting on and nothing running;
  - a geofence error re-armed without scheduling, from a branch that does
    not hold the broadcast open, so its callback may never land;
  - `ensure`, the "leave nothing unscheduled" fallback, consulted the stored
    `armed` belief — which at every one of its eight call sites has just been
    disproved. It is the adaptive interval again, unconditionally.

Sharper, in both levers the API gives:
  - radius 200 m to 150 m, the tightest value inside Google's documented
    100-150 m band, which is set by network-location accuracy because the
    fence is positioned from Wi-Fi and cell rather than GNSS;
  - responsiveness 5 min to 2 min. It is an entitlement to be late, not a
    sampling rate, and five minutes was longer than the whole end-to-end
    latency usually is. Not 0: the background limits put practical
    responsiveness near two minutes whatever is asked for, and the reference
    is explicit that a bigger value saves power, so 0 buys nothing and pays
    for it.

New(zh-Hant): 背景定位改用鬧鐘看門狗守住地理圍欄,並讓圍欄更靈敏
New(en-US): background location watchdogs the geofence and triggers sooner
`TEXT_MESSAGE_APP` is a port number, not a promise. Anything can be sent
on it, and the decoder ran `utf8.decode(payload, allowMalformed: true)` —
which substitutes U+FFFD per bad sequence and drops the bytes inside the
decoder. A binary body arrived in the chat as a row of  that nothing
could undo, because by then the only copy of it was gone.

So the decode is strict, and a body that is not text becomes a hex dump
at the one point that still has the bytes. The bubble labels it with its
size and sets it in monospace; long-press copies the dump, which pastes
straight into `xxd -r -p`.

Valid UTF-8 is not on its own enough, and assuming it was would have left
the symptom in place for the likeliest binary body of all. Every byte
below 0x80 decodes, so a protobuf misdirected onto the text port —
`08 01 10 05 18 02` — comes back as six control characters that draw as
tofu and read as something a person typed. A C0 control byte that is not
tab, newline or carriage return therefore means binary, which is the rule
git, file(1) and grep -I use. Non-empty bytes that decode to nothing mean
binary too: `utf8.decode` swallows a leading BOM, so a three-byte packet
was being reported to the user as an empty message.

Nothing wakes anybody for one. The notification body is built from the
same text, and a hex dump posted under a sender's name in the shade says
somebody typed it — the exact claim the bubble grew a label to avoid.

The flag is stored rather than re-derived, because it cannot be
re-derived: a hex dump is itself valid text, so by the time anything
downstream sees the string the distinction is gone. The column arrives by
ALTER and reads NULL as text, which is right — every row written before
it existed came out of a decoder that could not represent anything else.

New(zh-Hant): 二進位訊息在 Meshtastic 聊天中顯示為十六進位,不再是亂碼
New(en-US): a binary Meshtastic message is shown as hex instead of tofu
Optimization(zh-Hant): 本次更新頁全面美化,卡片更有層次與進場動畫
Optimization(en-US): Restyle the highlights page with layered cards and motion
It passed on 2026-08-18 and failed on the 19th, on nobody's change. The
row it inserts is stamped `DateTime.utc(2026, 8, 18)`, and `flush` prunes
anything older than `logRetention` in the same transaction as the insert
— so once real time moved a day on, the row was deleted by the call that
wrote it and `recent()` came back empty.

The store already takes a clock, and log_store_test.dart already injects
one. This one just did not.
The Apple Developer account was filling up with identical rows — TYPE
"Development", NAME "Created via API", one or two per push to main — and
every one of them was dead on arrival.

`-allowProvisioningUpdates` does what its man page says: "for automatically
signed targets, xcodebuild will create and update profiles, app IDs, and
certificates". The runner is ephemeral and its keychain is empty, so the
archive looks for a signing identity, finds none, and creates one. The
private key it generates never leaves that runner, so the row it leaves
behind in the account can never sign anything again. It is not a
certificate; it is a tombstone. They accumulate until the team hits Apple's
cap, and then every iOS release fails at once until a human revokes by hand.

Dropping the flag is not the fix — the archive then dies on "No profiles
for 'com.exptech.dpip.dpip' were found". Profile management is the half we
want and certificate creation is the half we do not, and the flag does not
split. Downloading an existing certificate is not the fix either: Apple
never held the private key. `CertificateCreateRequest` takes a `csrContent`,
so the caller keeps the key and sends only the public half.

So the runner is handed one, in a keychain under `$RUNNER_TEMP` that the VM
takes with it. Only the development half needs this: `-exportArchive`'s
distribution signing already works, because cloud-managed certificates keep
that private key on Apple's side. Cloud signing has no development
counterpart, and that asymmetry is why half this pipeline was quietly fine.

The script's one hard assertion is that a *valid* identity exists afterwards,
because every likely way to get the secret wrong is otherwise silent — the
release still goes green, still by minting. A .p12 exported from Keychain
Access's "Certificates" category has no private key; an expired certificate
imports cleanly; a chain that will not build imports cleanly. All three make
`security find-identity` print "0 valid identities found" and exit 0.
Everything else it reports is a warning: a release that dies because a
diagnostic could not parse a date is worse than the problem being fixed.

Needs two new repository secrets, APPLE_DEV_CERT_BASE64 and
APPLE_DEV_CERT_PASSWORD, before the next release runs.

Also corrects three files that pointed at `release-internal.yml`, which has
not existed for some time, and the ExportOptions header, which described
automatic signing as covering the whole job — the belief this bug lived in.
The LLM review is advice, and advice should never be the reason a pull
request cannot merge. It could be: a required check that FAILS blocks, and
this step fails for things that say nothing about the code under review —
the endpoint is down, the token expired, the quota ran out, the diff was
too big, the model returned something unparseable.

So the review step carries `continue-on-error`, and its cap moved from the
job to the step. That distinction is the whole change: a job-level timeout
fails the job and nothing downstream can absorb it, while a step-level one
is an error like any other. 15 minutes is well past a normal diff, so
reaching it means something is wedged rather than slow.

A green check that reviewed nothing is the obvious way for this to rot, so
a following step reads `outcome` — the real result, before
continue-on-error rewrote it — and turns anything but success into a
warning that says the pull request has not been reviewed and how to retry.

Also drops a stray `with:` from the action's input map. It parsed as an
input named `with` with a null value, which the action ignored.
`noteWake(kind)` writes `wake_${kind}_at` and `wake_${kind}_n`, so passing
it an exception name built a preference key per exception class —
`wake_report failed: UnknownHostException_at` — in a namespace with no
bound, in the file every receiver reads on every wake.

It also claimed a wake that never happened. The wake counters exist to
answer "did the OS call us at all", which was the question that found the
133-minute silence, and a failed POST is not an answer to it.

`note` is what that line meant: one breadcrumb in the ring, which is where
the rest of the report outcomes already go. No changelog line — nothing
here is visible outside the developer page.
@whes1015
whes1015 requested a review from a team as a code owner August 19, 2026 14:19
@whes1015
whes1015 enabled auto-merge (rebase) August 19, 2026 14:19
@whes1015
whes1015 disabled auto-merge August 19, 2026 16:18
@whes1015
whes1015 merged commit a6e18a5 into main Aug 19, 2026
5 of 6 checks passed
@whes1015
whes1015 deleted the fix/android-alerts-and-location branch August 19, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant