Skip to content

Commit ca9a06d

Browse files
committed
Enhance Pomodoro Timer
1 parent 7e26826 commit ca9a06d

File tree

175 files changed

+1682
-1434
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+1682
-1434
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ android {
4141
else -> 0
4242
}
4343

44-
val vCode = 373
44+
val vCode = 376
4545
versionCode = vCode - singleAbiNum
46-
versionName = "2.0.13"
46+
versionName = "2.0.14"
4747

4848
ndk {
4949
//noinspection ChromeOsAbiSupport

app/src/main/java/com/ismartcoding/plain/data/DPomodoroSettings.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ data class DPomodoroSettings(
1111
val pomodorosBeforeLongBreak: Int = 4,
1212
val showNotification: Boolean = true,
1313
val playSoundOnComplete: Boolean = true,
14+
val soundPath: String = "",
15+
val originalSoundName: String = "",
1416
) {
1517
fun getTotalSeconds(state: PomodoroState): Int {
1618
return when (state) {

app/src/main/java/com/ismartcoding/plain/enums/PickFileType.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ enum class PickFileTag {
1313
FEED,
1414
BOOK,
1515
RESTORE,
16+
POMODORO,
1617
}

app/src/main/java/com/ismartcoding/plain/events/AppEvents.kt

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.net.Uri
66
import android.os.PowerManager
77
import com.ismartcoding.lib.channel.Channel
88
import com.ismartcoding.lib.channel.ChannelEvent
9-
import com.ismartcoding.lib.channel.sendEvent
109
import com.ismartcoding.lib.helpers.CoroutinesHelper.coIO
1110
import com.ismartcoding.lib.helpers.CoroutinesHelper.coMain
1211
import com.ismartcoding.lib.logcat.LogCat
@@ -28,11 +27,6 @@ import com.ismartcoding.plain.features.bluetooth.BluetoothUtil
2827
import com.ismartcoding.plain.features.feed.FeedWorkerStatus
2928
import com.ismartcoding.plain.powerManager
3029
import com.ismartcoding.plain.services.HttpServerService
31-
import com.ismartcoding.plain.preference.PomodoroSettingsPreference
32-
import com.ismartcoding.plain.ui.MainActivity
33-
import com.ismartcoding.plain.ui.page.pomodoro.PomodoroHelper
34-
import com.ismartcoding.plain.ui.page.pomodoro.PomodoroHelper.playNotificationSound
35-
import com.ismartcoding.plain.ui.page.pomodoro.PomodoroState
3630
import com.ismartcoding.plain.web.AuthRequest
3731
import com.ismartcoding.plain.web.websocket.WebSocketHelper
3832
import io.ktor.server.websocket.DefaultWebSocketServerSession
@@ -110,15 +104,11 @@ class SleepTimerEvent(val durationMs: Long) : ChannelEvent()
110104

111105
class CancelSleepTimerEvent : ChannelEvent()
112106

113-
class PomodoroTimerEvent(val timeLeft: Int, val state: PomodoroState) : ChannelEvent()
114-
115-
class CancelPomodoroTimerEvent : ChannelEvent()
116-
117107
object AppEvents {
118108
private lateinit var mediaPlayer: MediaPlayer
119109
private var mediaPlayingUri: Uri? = null
120110
private var sleepTimerJob: Job? = null
121-
private var pomodoroTimerJob: Job? = null
111+
122112
val wakeLock: PowerManager.WakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "${BuildConfig.APPLICATION_ID}:http_server")
123113

124114

@@ -158,37 +148,6 @@ object AppEvents {
158148
sleepTimerJob = null
159149
}
160150

161-
is PomodoroTimerEvent -> {
162-
pomodoroTimerJob?.cancel()
163-
pomodoroTimerJob = coIO {
164-
delay(event.timeLeft * 1000L)
165-
MainActivity.instance.get()?.pomodoroVM?.let { vm ->
166-
when (vm.currentState.value) {
167-
PomodoroState.WORK -> vm.handleWorkSessionCompleteAsync(isSkip = false)
168-
PomodoroState.SHORT_BREAK, PomodoroState.LONG_BREAK -> vm.handleBreakSessionComplete()
169-
}
170-
vm.resetSessionState()
171-
}
172-
val context = MainApp.instance
173-
val settings = PomodoroSettingsPreference.getValueAsync(context)
174-
if (settings.showNotification) {
175-
PomodoroHelper.showNotificationAsync(context, event.state)
176-
}
177-
if (settings.playSoundOnComplete) {
178-
try {
179-
PomodoroHelper.playNotificationSound()
180-
} catch (e: Exception) {
181-
LogCat.e("Failed to play Pomodoro sound: ${e.message}")
182-
}
183-
}
184-
}
185-
}
186-
187-
is CancelPomodoroTimerEvent -> {
188-
pomodoroTimerJob?.cancel()
189-
pomodoroTimerJob = null
190-
}
191-
192151
is WebSocketEvent -> {
193152
coIO {
194153
WebSocketHelper.sendEventAsync(event)

app/src/main/java/com/ismartcoding/plain/events/HttpApiEvents.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ class HttpApiEvents {
1111
class MessageUpdatedEvent(val id: String) : ChannelEvent()
1212

1313
// Pomodoro events
14-
class PomodoroStartEvent : ChannelEvent()
14+
class PomodoroStartEvent(val timeLeft: Int) : ChannelEvent()
1515

1616
class PomodoroPauseEvent : ChannelEvent()
1717

1818
class PomodoroStopEvent : ChannelEvent()
19-
20-
class PomodoroProgressUpdateEvent(val timeLeft: Int) : ChannelEvent()
2119
}

app/src/main/java/com/ismartcoding/plain/services/HttpServerService.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ class HttpServerService : LifecycleService() {
5757
super.onStartCommand(intent, flags, startId)
5858

5959
try {
60-
NotificationHelper.ensureDefaultChannel()
61-
6260
val notification = NotificationHelper.createServiceNotification(
6361
this,
6462
"${BuildConfig.APPLICATION_ID}.action.stop_http_server",

app/src/main/java/com/ismartcoding/plain/ui/models/PomodoroViewModel.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import androidx.compose.runtime.mutableIntStateOf
55
import androidx.compose.runtime.mutableStateOf
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
8-
import com.ismartcoding.lib.channel.sendEvent
8+
import com.ismartcoding.lib.logcat.LogCat
9+
import com.ismartcoding.plain.MainApp
910
import com.ismartcoding.plain.data.DPomodoroSettings
1011
import com.ismartcoding.plain.db.AppDatabase
1112
import com.ismartcoding.plain.db.DPomodoroItem
12-
import com.ismartcoding.plain.events.CancelPomodoroTimerEvent
13-
import com.ismartcoding.plain.events.PomodoroTimerEvent
1413
import com.ismartcoding.plain.preference.PomodoroSettingsPreference
14+
import com.ismartcoding.plain.ui.page.pomodoro.PomodoroHelper
1515
import com.ismartcoding.plain.ui.page.pomodoro.PomodoroState
1616
import kotlinx.coroutines.Dispatchers
1717
import kotlinx.coroutines.Job
@@ -56,7 +56,6 @@ class PomodoroViewModel : ViewModel() {
5656
fun startSession() {
5757
isRunning.value = true
5858
isPaused.value = false
59-
sendEvent(PomodoroTimerEvent(timeLeft.intValue, currentState.value))
6059
startCountdownTimer()
6160
}
6261

@@ -69,6 +68,24 @@ class PomodoroViewModel : ViewModel() {
6968
timeLeft.intValue--
7069
}
7170
}
71+
72+
// When timer reaches zero, send completion event
73+
if (isRunning.value && !isPaused.value && timeLeft.intValue <= 0) {
74+
val context = MainApp.instance
75+
if (settings.value.showNotification) {
76+
PomodoroHelper.showNotificationAsync(context, currentState.value)
77+
}
78+
try {
79+
PomodoroHelper.playCompletionSound(context, settings.value)
80+
} catch (e: Exception) {
81+
LogCat.e("Failed to play Pomodoro sound: ${e.message}")
82+
}
83+
when (currentState.value) {
84+
PomodoroState.WORK -> handleWorkSessionCompleteAsync(isSkip = false)
85+
PomodoroState.SHORT_BREAK, PomodoroState.LONG_BREAK -> handleBreakSessionComplete()
86+
}
87+
resetSessionState()
88+
}
7289
}
7390
}
7491

@@ -87,7 +104,6 @@ class PomodoroViewModel : ViewModel() {
87104
private fun stopTimer() {
88105
timerJob?.cancel()
89106
timerJob = null
90-
sendEvent(CancelPomodoroTimerEvent())
91107
}
92108

93109
private fun resetToInitialState() {
@@ -176,6 +192,5 @@ class PomodoroViewModel : ViewModel() {
176192
super.onCleared()
177193
timerJob?.cancel()
178194
eventHandler?.cancel()
179-
sendEvent(CancelPomodoroTimerEvent())
180195
}
181196
}

app/src/main/java/com/ismartcoding/plain/ui/page/Main.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ fun Main(
205205
}
206206

207207
is HttpApiEvents.PomodoroStartEvent -> {
208+
pomodoroVM.timeLeft.intValue = event.timeLeft
208209
pomodoroVM.startSession()
209210
}
210211

@@ -216,11 +217,6 @@ fun Main(
216217
pomodoroVM.resetTimer()
217218
}
218219

219-
is HttpApiEvents.PomodoroProgressUpdateEvent -> {
220-
pomodoroVM.timeLeft.intValue = event.timeLeft
221-
pomodoroVM.startSession()
222-
}
223-
224220
else -> {
225221
// Handle other events if necessary
226222
}

app/src/main/java/com/ismartcoding/plain/ui/page/pomodoro/PomodoroHelper.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import android.content.Context
66
import android.content.Intent
77
import android.media.AudioAttributes
88
import android.media.AudioFormat
9-
import android.media.AudioManager
109
import android.media.AudioTrack
1110
import androidx.core.app.NotificationCompat
1211
import androidx.core.app.NotificationManagerCompat
12+
import com.ismartcoding.lib.extensions.getFinalPath
13+
import com.ismartcoding.lib.extensions.isAudioFast
1314
import com.ismartcoding.lib.helpers.CoroutinesHelper
1415
import com.ismartcoding.lib.helpers.CoroutinesHelper.coIO
1516
import com.ismartcoding.lib.logcat.LogCat
1617
import com.ismartcoding.plain.Constants
1718
import com.ismartcoding.plain.R
19+
import com.ismartcoding.plain.data.DPlaylistAudio
20+
import com.ismartcoding.plain.data.DPomodoroSettings
1821
import com.ismartcoding.plain.db.AppDatabase
22+
import com.ismartcoding.plain.features.AudioPlayer
1923
import com.ismartcoding.plain.features.Permission
2024
import com.ismartcoding.plain.features.locale.LocaleHelper
2125
import com.ismartcoding.plain.helpers.NotificationHelper
@@ -28,6 +32,7 @@ import kotlin.math.PI
2832
import kotlin.math.exp
2933
import kotlin.math.ln
3034
import kotlin.math.sin
35+
import java.io.File
3136

3237
object PomodoroHelper {
3338
@SuppressLint("MissingPermission")
@@ -95,6 +100,48 @@ object PomodoroHelper {
95100
}
96101
}
97102

103+
suspend fun playCompletionSound(context: Context, settings: DPomodoroSettings) {
104+
// First check if sound should be played at all
105+
if (!settings.playSoundOnComplete) {
106+
return
107+
}
108+
109+
if (settings.soundPath.isNotEmpty()) {
110+
try {
111+
val actualPath = settings.soundPath.getFinalPath(context)
112+
val file = File(actualPath)
113+
114+
if (file.exists() && actualPath.isAudioFast()) {
115+
playCustomSong(context, actualPath)
116+
return
117+
} else if (settings.soundPath.startsWith("content://")) {
118+
playCustomSong(context, settings.soundPath)
119+
return
120+
}
121+
} catch (e: Exception) {
122+
LogCat.e("Failed to play custom song, falling back to default sound: ${e.message}")
123+
// Fall through to play default sound
124+
}
125+
}
126+
127+
// Play default notification sound in IO thread
128+
coIO {
129+
playNotificationSound()
130+
}
131+
}
132+
133+
private suspend fun playCustomSong(context: Context, songPath: String) {
134+
try {
135+
val audio = DPlaylistAudio.fromPath(context, songPath)
136+
coIO {
137+
AudioPlayer.justPlay(context, audio)
138+
}
139+
} catch (e: Exception) {
140+
LogCat.e("Failed to play custom song: ${e.message}")
141+
// Don't throw exception, let caller handle fallback
142+
}
143+
}
144+
98145
fun playNotificationSound() {
99146
val sampleRate = 44100
100147
val durationMs = 300 // 0.3 seconds

0 commit comments

Comments
 (0)