Skip to content

Commit c217a41

Browse files
author
Anton
committed
v1.1.11: Fix phase transition sound race condition in background
1 parent 43ae60b commit c217a41

4 files changed

Lines changed: 23 additions & 9 deletions

File tree

GEMINI.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
## 專案現況
44
- **專案名稱**: Clock
5-
- **目前版本**: v1.1.7 (Background Precision & Deduplication)
5+
- **目前版本**: v1.1.11 (Phase Transition Sound Fix)
66
- **核心技術**: .NET 10, WPF, Android Native (Kotlin + Compose), SignalR, mDNS
77
- **開發日期**: 2026-02-01
88

99
## 專案結構
1010
- clock: WPF 視圖層、通訊服務器實作與程式入口。支援 WinExe 模式與動態控制台分配。
1111
- clock.Lib: 核心邏輯、模型與 ViewModel。已重構為純 .NET 10 Library,與 UI 完全解耦。
12-
- clock-android: Android 原生專案 (Android Studio),負責手機端同步顯示、背景服務與本地設定
12+
- clock-android: Android 原生專案 (Android Studio),負責手機端同步顯示、服務與本地設定
1313
- Tests/clock.IntegrationTests: 整合測試,包含計時器、狀態同步、設定持久化與網路診斷測試。
1414
- conductor/: AI 導引開發軌跡資料夾 (包含詳細的 Track 紀錄與 Plan/Spec)。
1515

@@ -29,6 +29,10 @@
2929
- **本地目標時間機制**: Android Engine 改用 `localTargetEndTime` 模型,確保本地模式下背景掛起後的恢復準確度。
3030
- **服務生存強化**: 移除 `onTaskRemoved` 中的 `stopSelf`。即使使用者滑掉 App,Foreground Service 仍會維持運作。
3131
- **Drift 門檻最佳化**: 引入 2 秒 Drift 門檻(基於絕對目標時間),降低 `AlarmManager` 更新頻率。
32+
10. **階段轉場音效修正 (v1.1.11)**:
33+
- **移除引擎自切換**: 解決 `PomodoroEngine` 內部循環與 `TimerService` 鬧鐘的競爭問題,防止因 Loop 搶先切換導致即將播放的音效任務被 Cancel。
34+
- **轉場偵測播放**: 在 `isWorkPhase` 監聽器中實作 `lastObservedPhase` 檢查,確保無論是本地計時到期、手動 Skip 還是 PC 端同步切換,都能正確觸發上一階段的結束音效。
35+
- **順序保證**: 嚴格執行「播放音效 -> 切換階段 -> 預約下一次」的執行序鏈,徹底解決黑畫面切換時無聲的問題。
3236

3337
## 重要技術決策
3438
- **雙軌音效觸發 (Dual-Track)**:

clock-android/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212
minSdk = 24
1313
targetSdk = 34
1414
versionCode = 1
15-
versionName = "1.1.7"
15+
versionName = "1.1.11"
1616

1717
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1818
vectorDrawables {

clock-android/app/src/main/kotlin/com/anton/clock/core/PomodoroEngine.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,8 @@ class PomodoroEngine(
109109
if (newVal > 0) {
110110
_remainingSeconds.value = newVal
111111
} else {
112-
// 倒數結束,自動切換階段 (離線模式)
112+
// 倒數結束,僅設為 0,不再主動切換階段,交由 Service 處理音效後切換
113113
_remainingSeconds.value = 0.0
114-
lastTime = now // 更新時間基準,避免切換後 delta 累計
115-
localTogglePhase()
116-
return@launch // 跳出舊 Loop,localTogglePhase 會啟動新的
117114
}
118115
}
119116
lastTime = now

clock-android/app/src/main/kotlin/com/anton/clock/core/TimerService.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class TimerService : Service() {
4646
private var soundJob: Job? = null
4747
private var lastScheduledTargetEnd: Long = 0L
4848
private var lastPlayedTargetTime: Long = 0L
49+
private var lastObservedPhase: Boolean? = null
4950

5051
private val screenReceiver = object : BroadcastReceiver() {
5152
override fun onReceive(context: Context?, intent: Intent?) {
@@ -137,8 +138,14 @@ class TimerService : Service() {
137138
}
138139

139140
serviceScope.launch {
140-
engine.isWorkPhase.collect {
141-
lastScheduledTargetEnd = 0L // 重置以確保切換階段時重新預約
141+
engine.isWorkPhase.collect { isWork ->
142+
if (lastObservedPhase != null && lastObservedPhase != isWork) {
143+
// 階段切換了!播放「剛結束」階段的音效(對應上一筆預約時間)
144+
// 此處對應 Sync 模式下 PC 切換階段,或手動 Skip 的音效觸發
145+
playSound(lastScheduledTargetEnd, "PhaseTransition")
146+
}
147+
lastObservedPhase = isWork
148+
lastScheduledTargetEnd = 0L // 重置以確保重新預約
142149
scheduleSound()
143150
updateNotification()
144151
}
@@ -217,6 +224,12 @@ class TimerService : Service() {
217224
if (!engine.isPaused.value) {
218225
Log.d(TAG, "Coroutine sound trigger fired for $targetEndTime")
219226
playSound(targetEndTime, "Coroutine")
227+
228+
// 如果是在 Local 模式 (未連線),由 Coroutine 負責觸發階段切換
229+
if (!engine.isSynced.value) {
230+
Log.d(TAG, "Local mode: Coroutine triggered phase toggle")
231+
engine.localTogglePhase()
232+
}
220233
}
221234
}
222235
}

0 commit comments

Comments
 (0)