Repository navigation
fix(reminder): 三档铃声 + 原生时间触发 + 后台守护补挂 - #355
Merged
Merged
Conversation
…red ring sound JS's 30s poll used to race the native AlarmManager alarm for every time-type reminder — whichever fired first won, and the JS path would actively cancel the (more reliable) native alarm on a win. That race is why background delivery looked intermittent: whenever the JS timer happened to still be alive when a reminder came due, it would hijack delivery from the native alarm and fall back to a weaker, easy-to-miss channel. runHandleTime() now skips any time-type schedule that already has a native alarm registered, full stop — native ownership is no longer just a same-tick head start, it's exclusive. Alongside that: every strength now surfaces through the native full-screen ring page (via presentNow) instead of low strength getting a plain system notification, with a three-tier ring sound (low/medium = a one-shot ping, high keeps the looping speech) replacing the old sound on/off boolean end to end. Also fixed two adjacent notification bugs — the notifications permission gap was never surfaced to the permission-nudge dialog, and ExpoSystemNotification shared an Android notification channel with the geofence path despite the two wanting opposite sound behavior (channel sound is immutable after creation on Android 8+, so whichever side created it first won for both) — plus added native fallback notifications for the two previously-silent delivery failure paths (OEM-blocked foreground service start, and an alarm queued behind a still-open ring page). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… sound commit DeliveryChannel/AlarmSchedulerPort's interface barrel were missing the native_full_screen/presentNow additions this commit's history depends on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…us location Native alarms and system geofencing cover the common case, but two gaps had no safety net at all: a time-type reminder whose native alarm never got scheduled (e.g. exact-alarm permission missing) had nothing watching it in the background, and Android's Fused Location Provider is opportunistic enough that geofence transitions can lag well behind the actual crossing when nothing else is driving fresh location fixes. Rather than hand-rolling a new native foreground service, this reuses expo-location's already-configured startLocationUpdatesAsync foreground service (ACCESS_BACKGROUND_LOCATION / FOREGROUND_SERVICE_LOCATION and isAndroidForegroundServiceEnabled were already wired up in app.config.js for the geofencing path). ReminderGuardCoordinator starts/stops/reconfigures it based on whether anything is still unconfirmed, and reminderGuardTask.ts (a TaskManager-defined task, same shape as the existing geofenceTask.ts) does the actual work on every wake: routes live samples into the existing handleLocation()/evaluateGeofence() path when a session is alive, falls back to a direct-SQLite pass mirroring deliverHeadlessGeofenceEvent() when headless, re-presents any time-type reminder whose trigger has passed with no native alarm currently armed (checked via a new hasArmedAlarm bridge method reading AlarmScheduler's persisted alarm list, since in-memory registrations aren't visible from a headless context), and re-presents anything stuck in a pending disposition for over two minutes as a safety net for deliveries that fired natively but never reached the user. Poll interval is a distance-based interpolation (15s near a target, 5min far from all of them) recomputed on every schedule change and every sample, not a fixed cadence. The 'return_to_recorded_location' mode's recorded_location isn't persisted to SQLite yet (confirmed against the three existing readers, which all hardcode it to null) — this task's headless pass inherits that same pre-existing gap rather than working around it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eftover diagnostics The presentNow feature (native full-screen delivery for both time and location schedules) originally shipped bundled with geofence diagnostics in a single upstream commit; cherry-picking only the tiered-sound commit missed several pieces presentNow depends on: - AlarmScheduler.immediateRequestCode() - AlarmSchedulerPort/barrel-export additions for AlarmPresentationRequest/Receipt - the placeholder-registration + awaited location listener behavior in LocalReminderApplication.rebuildInternal()/watchLocation() - AlarmScheduler.schedule() call sites in RingActivity/AlarmSoundService missing the speechText param after the signature changed Also strips the geofence diagnostic recording that came along for the ride in geofenceTask.ts, and updates AlarmIntentForwardingTest.java to the current sound_tier-based contract instead of the old boolean sound field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…d location permission Fixes review findings on this PR: ensureLocationUpdates() required both foreground AND background location permission before calling startLocationUpdatesAsync(), even for schedules with no location component at all. On Android, with the exact-alarm permission denied (the case the fallback exists for) and only "while using the app" location granted (no "always allow", a very common choice), the guard task -- and with it runTimeFallbackPass()/runStuckPendingPass(), which run on every wakeup regardless of whether a location sample arrived -- never got registered, so overdue time reminders were never rescued. expo-location's own native module only requires ACCESS_BACKGROUND_LOCATION when starting location updates without a foregroundService config (LocationModule.kt: "as a user-initiated foreground service with notification, this does NOT require the background location permission"). Since this call always passes foregroundService, the background permission check was stricter than what the platform actually needs and is safe to drop; the foreground check stays, since the native side still requires it unconditionally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…outing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e guards Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yyy-router
approved these changes
Aug 24, 2026
Contributor
Author
|
关于 reminderGuardTask.ts / geofenceTask.ts / LocalReminderApplication.ts 的 patch coverage:
|
1 task done
LUPENGHAN
added a commit
that referenced
this pull request
Aug 24, 2026
* feat(reminder): add local TTS for high-strength alarms Adds on-device text-to-speech for high-strength reminders, replacing the discarded upstream design (which spoke every alarm with sound enabled) with a strength-gated one layered on top of the existing tiered ring sound: only reminder_strength === 'high' triggers speech, via AlarmSoundTier 'full' + a non-empty speech_text. - composeReminderSpeech()/strengthDelivery.ts builds the spoken text as "title, time to go, it's now HH:MM" (or a date-based line for all-day schedules), threaded through AlarmSchedulerPort -> NativeAlarmScheduler -> TimeflowAlarmBridge -> AlarmModule -> AlarmScheduler/AlarmReceiver/AlarmSoundService/RingActivity. - AlarmTtsEngine.kt: a shared TextToSpeech singleton, eagerly bound at AlarmModule construction time (app-launch, almost always foreground) rather than lazily inside AlarmSoundService.onCreate() (often background/restricted) -- reduces the risk of an Android background process restriction blocking the engine bind. - AndroidManifest.xml <queries> entry for android.intent.action.TTS_SERVICE: without it, targetSdk 30+ package visibility hides installed TTS engines from PackageManager and TextToSpeech init fails with status=ERROR even when an engine is genuinely installed and works from system Settings. - Drops upstream's ReminderSpeechFormatter.java/reminderSpeech.ts (the "speak every alarm" formatter) and the now-fully-redundant plugins/withTimeflowAlarm.js, whose permission declarations were already covered by the module's own AndroidManifest.xml. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(reminder): drop leftover TTS diagnostic logs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reminder): forward speech_text placeholder arg in presentNow assertions PR1's presentNow tests (now on main via #355) assert 6 native args; this branch's TTS commit added a 7th speech_text param to presentNow's native call. Update the two assertions to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reminder): speak the effective trigger time, fall back on synchronous TTS enqueue failure Fixes two P1 findings from fennoai's review of #363: - composeReminderSpeech() formatted schedule.start_time unconditionally, but the alarm actually fires at resolveEffectiveTriggerAt(schedule) -- earlier than start_time for before_start reminders, and snoozed_until once snoozed. A 15-minutes-before reminder announced the event's start time instead of the moment it actually rang, and a snoozed reminder kept repeating the original stale time. - AlarmTtsEngine.speak() returns TextToSpeech.ERROR when the engine can't enqueue speech synchronously, but speakCurrent() ignored the return value. No utterance callback fires in that case, so onError() is never reached, and by then the bundled-audio fallback has already been stopped -- the high-strength alarm goes silent. Check the return value and route into the same onSpeechError() fallback used by the async error path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reminder): drop remaining leftover TTS diagnostic logs 6ba1979 already removed two of these "定位问题排查完可以删" markers but missed four more with the same marker, still firing unconditionally: - reminderGuardTask.ts: [guard] tick fired on every single guard task wakeup (as often as every 15s in the densest polling tier), and [guard] geofence eval fired on every headless location evaluation. - LocalReminderApplication.ts: the foreground-session equivalents, applyLocationSample and geofence eval, fired on every location callback. Also drops distanceMeters/center, which only existed to feed the deleted logs.
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.
Closes #354
这个 PR 做了什么
1. 原生闹钟三档铃声(none/ping/full)
AlarmContract.SOUND_TIER_*,按提醒强度换算:低=ping 不震动,中=ping+震动,高=循环响铃(full)+震动。RingActivity/AlarmSoundService统一按soundTier播放,不再是简单的布尔sound开关。2. 原生闹钟接管时间型提醒的触发与响铃
RingActivity/AlarmSoundService;JS 侧不再弹 Alert/放音跟原生抢事件。3. 后台守护任务:时间兜底轮询 + 持续定位
ReminderGuardCoordinator+reminderGuardTask:定期核对"该挂上原生闹钟的日程有没有真的挂上",没挂上就补挂。4. presentNow 统一全屏响铃路径
presentNow原生全屏响铃优先路径;只有presentNow本身不可用(iOS、或原生模块拿不到)时才回退到 JS 通道。验证
npx tsc --noEmit:通过npx jest:661/661 通过:timeflow-alarm:testDebugUnitTest把关)。拆分说明
这是一条更大分支(地理围栏后台提醒 + 本地 TTS)拆出来的第一个 PR,按依赖顺序还有:
🤖 Generated with Claude Code