diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 31f5989..d1dc0f8 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -9,7 +9,8 @@
"felipecaslazana.vscode-android-logcat",
"ms-azuretools.vscode-docker",
"fwcd.kotlin",
- "vscjava.vscode-java-pack"
+ "vscjava.vscode-java-pack",
+ "Anthropic.claude-code"
]
}
},
diff --git a/CLAUDE.md b/CLAUDE.md
index aff759f..b8f1d7b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,16 +17,19 @@
- **Android Patterns**: Use `findViewById` for view binding in this project to maintain consistency with existing simple architecture.
## Architecture
-- **Pattern**: Simple Activity-Dialog architecture.
-- **State**: The countdown state is managed locally within `CountdownDialog` using `android.os.CountDownTimer`.
+- **Pattern**: Activity-Service architecture with `ForegroundService`.
+- **State**: The countdown state is managed in `CountdownService` using `android.os.CountDownTimer`, persisted to `SharedPreferences` for process death recovery.
+- **UI**: `MainActivity` binds to the service and acts as a thin UI projection, receiving state updates via `TimerStateListener` callback.
- **Dependency Management**: Gradle with Groovy DSL.
- **Key Constraints**:
- - The `CountdownDialog` is explicitly set to `setCancelable(false)` and `setCanceledOnTouchOutside(false)` to ensure the timer completes.
+ - The notification is set to `setOngoing(true)` — cannot be swiped away.
+ - Timer state survives process death via `SharedPreferences` persistence.
## Critical Files Reference
-- `app/src/main/java/com/example/countdowntimer/MainActivity.kt`: Entry point and trigger for the timer.
-- `app/src/main/java/com/example/countdowntimer/CountdownDialog.kt`: Implementation of the countdown logic and `ToneGenerator` trigger.
+- `app/src/main/java/com/example/countdowntimer/MainActivity.kt`: Entry point, binds to service, implements `TimerStateListener`.
+- `app/src/main/java/com/example/countdowntimer/CountdownService.kt`: ForegroundService owning timer logic, `ToneGenerator`, `Vibrator`, notification, and state persistence.
+- `app/src/main/java/com/example/countdowntimer/TimerStateListener.kt`: Callback interface for service-to-activity state updates.
- `app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt`: Custom view for the circular progress ring and countdown text.
-- `app/src/main/res/layout/activity_main.xml`: Main screen layout.
-- `app/src/main/res/layout/dialog_countdown.xml`: Timer dialog layout.
-- `app/build.gradle`: Build configuration, SDK versions (minSdk 24, targetSdk 34), and dependencies.
+- `app/src/main/res/layout/activity_main.xml`: Main screen layout (contains both picker and countdown UI).
+- `app/src/main/AndroidManifest.xml`: Declares service and foreground service permissions.
+- `app/build.gradle`: Build configuration, SDK versions (minSdk 34, targetSdk 34), and dependencies.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0484d98..ab30fb3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,8 @@
+
+
+
diff --git a/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt b/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt
deleted file mode 100644
index 0f84577..0000000
--- a/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.example.countdowntimer
-
-import android.app.Dialog
-import android.content.Context
-import android.content.SharedPreferences
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.ColorUtils
-import android.media.AudioManager
-import android.media.ToneGenerator
-import android.os.Bundle
-import android.os.CountDownTimer
-import android.os.Vibrator
-import android.view.View
-import android.view.WindowManager
-import android.widget.Button
-
-class CountdownDialog(
- private val context: Context,
- private val durationMillis: Long,
- private val prefs: SharedPreferences
-) : Dialog(context) {
- private lateinit var cvCountdown: CountdownCanvasView
- private lateinit var btnStop: View
- private lateinit var btnRestart: View
- private lateinit var btnDismiss: Button
- private var timer: CountDownTimer? = null
- private var toneGenerator: ToneGenerator? = null
- private var vibrator: Vibrator? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.dialog_countdown)
-
- setCanceledOnTouchOutside(false)
- setCancelable(false)
-
- window?.let { win ->
- win.setBackgroundDrawableResource(android.R.color.transparent)
- val lp = win.attributes
- lp.width = WindowManager.LayoutParams.MATCH_PARENT
- lp.height = WindowManager.LayoutParams.MATCH_PARENT
- win.attributes = lp
- }
-
- cvCountdown = findViewById(R.id.cvCountdown)
- btnStop = findViewById(R.id.btnStop)
- btnRestart = findViewById(R.id.btnRestart)
- btnDismiss = findViewById(R.id.btnDismiss)
- toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
- vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
-
- btnStop.setOnClickListener { cancelTimer() }
- btnRestart.setOnClickListener { restartTimer() }
- btnDismiss.setOnClickListener { dismiss() }
-
- startTimer()
- }
-
- private fun startTimer() {
- cvCountdown.setProgress(1f)
- cvCountdown.setColor(ContextCompat.getColor(context, R.color.md_color_ring_progress))
- timer = object : CountDownTimer(durationMillis, 1000) {
- override fun onTick(millisUntilFinished: Long) {
- val progress = millisUntilFinished.toFloat() / durationMillis
- cvCountdown.setProgress(progress)
-
- // Interpolate ring color: green → yellow at 50% → red at end
- val green = android.graphics.Color.parseColor("#2E7D32")
- val yellow = android.graphics.Color.parseColor("#FFC107")
- val red = android.graphics.Color.parseColor("#F44336")
- val ringColor = when {
- progress >= 0.5f -> {
- val t = (progress - 0.5f) * 2f
- ColorUtils.blendARGB(green, yellow, t)
- }
- else -> {
- val t = progress * 2f
- ColorUtils.blendARGB(yellow, red, t)
- }
- }
- cvCountdown.setColor(ringColor)
-
- val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
- cvCountdown.setText(secondsLeft.toString())
-
- if (secondsLeft >= 1 && secondsLeft <= 3) {
- playTone(ToneGenerator.TONE_PROP_BEEP)
- if (prefs.getBoolean("vibration", true)) {
- vibrator?.vibrate(50)
- }
- }
- }
-
- override fun onFinish() {
- cvCountdown.setText(context.getString(R.string.timer_zero))
- cvCountdown.setProgress(0f)
- playTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
- if (prefs.getBoolean("vibration", true)) {
- vibrator?.vibrate(200)
- }
- btnStop.visibility = View.GONE
- btnRestart.visibility = View.GONE
- btnDismiss.visibility = View.VISIBLE
- }
- }.start()
- }
-
- private fun cancelTimer() {
- timer?.cancel()
- dismiss()
- }
-
- private fun restartTimer() {
- timer?.cancel()
- startTimer()
- }
-
- override fun dismiss() {
- timer?.cancel()
- toneGenerator?.release()
- toneGenerator = null
- super.dismiss()
- }
-
- private fun playTone(tone: Int, durationMillis: Int? = null) {
- if (durationMillis == null) {
- toneGenerator?.startTone(tone)
- } else {
- toneGenerator?.startTone(tone, durationMillis)
- }
- }
-}
diff --git a/app/src/main/java/com/example/countdowntimer/CountdownService.kt b/app/src/main/java/com/example/countdowntimer/CountdownService.kt
new file mode 100644
index 0000000..503e773
--- /dev/null
+++ b/app/src/main/java/com/example/countdowntimer/CountdownService.kt
@@ -0,0 +1,263 @@
+package com.example.countdowntimer
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.media.AudioManager
+import android.media.ToneGenerator
+import android.os.Binder
+import android.os.CountDownTimer
+import android.os.IBinder
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.os.VibratorManager
+import androidx.core.app.NotificationCompat
+import androidx.core.graphics.ColorUtils
+
+class CountdownService : Service() {
+
+ companion object {
+ const val ACTION_START = "com.example.countdowntimer.START"
+ const val ACTION_STOP = "com.example.countdowntimer.STOP"
+ const val ACTION_RESTART = "com.example.countdowntimer.RESTART"
+ const val EXTRA_DURATION = "extra_duration"
+
+ private const val NOTIFICATION_ID = 1
+ private const val CHANNEL_ID = "countdown_channel"
+ private const val CHANNEL_NAME = "Countdown Timer"
+ private const val PREFS_NAME = "timer_prefs"
+ private const val KEY_REMAINING_MILLIS = "remaining_millis"
+ private const val KEY_TOTAL_DURATION = "total_duration"
+ private const val KEY_TIMER_RUNNING = "timer_running"
+ }
+
+ private var countDownTimer: CountDownTimer? = null
+ private var toneGenerator: ToneGenerator? = null
+ private var vibrator: Vibrator? = null
+ private var prefs: SharedPreferences? = null
+ private var stateListener: TimerStateListener? = null
+ private var durationMillis: Long = 0
+ private var notificationManager: NotificationManager? = null
+ private var isTimerRunning: Boolean = false
+
+ private val binder = LocalBinder()
+
+ inner class LocalBinder : Binder() {
+ fun getService(): CountdownService = this@CountdownService
+ }
+
+ fun setStateListener(listener: TimerStateListener?) {
+ stateListener = listener
+ }
+
+ fun isRunning(): Boolean = isTimerRunning
+
+ // ---- Service lifecycle ----
+
+ override fun onCreate() {
+ super.onCreate()
+
+ prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
+
+ val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
+ vibrator = vibratorManager.defaultVibrator
+
+ createNotificationChannel()
+
+ // Restore timer state if it was running before process death
+ if (prefs?.getBoolean(KEY_TIMER_RUNNING, false) == true) {
+ val remaining = prefs?.getLong(KEY_REMAINING_MILLIS, 0) ?: 0
+ durationMillis = prefs?.getLong(KEY_TOTAL_DURATION, 0) ?: 0
+ if (remaining > 0 && durationMillis > 0) {
+ resumeTimer(remaining)
+ }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val action = intent?.action
+ when (action) {
+ ACTION_START -> {
+ val duration = intent.getLongExtra(EXTRA_DURATION, 600000L)
+ startTimer(duration)
+ }
+ ACTION_STOP -> stopTimer()
+ ACTION_RESTART -> restartTimer()
+ }
+ return START_NOT_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder = binder
+
+ override fun onDestroy() {
+ countDownTimer?.cancel()
+ countDownTimer = null
+ toneGenerator?.release()
+ toneGenerator = null
+ notificationManager?.cancel(NOTIFICATION_ID)
+ super.onDestroy()
+ }
+
+ // ---- Timer logic ----
+
+ private fun startTimer(duration: Long) {
+ durationMillis = duration
+ countDownTimer?.cancel()
+ countDownTimer = createCountDownTimer(duration).start()
+ isTimerRunning = true
+
+ persistState(duration, duration)
+ startForeground(NOTIFICATION_ID, buildNotification(duration, duration))
+ }
+
+ private fun resumeTimer(remaining: Long) {
+ isTimerRunning = true
+ countDownTimer = createCountDownTimer(remaining).start()
+ startForeground(NOTIFICATION_ID, buildNotification(remaining, durationMillis))
+ }
+
+ fun stopTimer() {
+ countDownTimer?.cancel()
+ countDownTimer = null
+ isTimerRunning = false
+
+ prefs?.edit()?.apply {
+ putBoolean(KEY_TIMER_RUNNING, false)
+ remove(KEY_REMAINING_MILLIS)
+ remove(KEY_TOTAL_DURATION)
+ }?.apply()
+
+ notificationManager?.cancel(NOTIFICATION_ID)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ stateListener?.onTimerStopped()
+ }
+
+ fun restartTimer() {
+ countDownTimer?.cancel()
+ startTimer(durationMillis)
+ }
+
+ private fun persistState(remaining: Long, total: Long) {
+ prefs?.edit()?.apply {
+ putLong(KEY_REMAINING_MILLIS, remaining)
+ putLong(KEY_TOTAL_DURATION, total)
+ putBoolean(KEY_TIMER_RUNNING, true)
+ }?.apply()
+ }
+
+ private fun createCountDownTimer(remaining: Long): CountDownTimer {
+ return object : CountDownTimer(remaining, 1000) {
+ override fun onTick(millisUntilFinished: Long) {
+ val progress = millisUntilFinished.toFloat() / durationMillis
+
+ // Persist remaining time
+ persistState(millisUntilFinished, durationMillis)
+
+ // Color interpolation: green → yellow at 50% → red at end
+ val green = android.graphics.Color.parseColor("#2E7D32")
+ val yellow = android.graphics.Color.parseColor("#FFC107")
+ val red = android.graphics.Color.parseColor("#F44336")
+ val ringColor = when {
+ progress >= 0.5f -> {
+ val t = (progress - 0.5f) * 2f
+ ColorUtils.blendARGB(green, yellow, t)
+ }
+ else -> {
+ val t = progress * 2f
+ ColorUtils.blendARGB(yellow, red, t)
+ }
+ }
+
+ val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
+
+ // Warning beeps and vibration in last 3 seconds
+ if (secondsLeft >= 1 && secondsLeft <= 3) {
+ toneGenerator?.startTone(ToneGenerator.TONE_PROP_BEEP)
+ vibrator?.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
+ }
+
+ // Update notification
+ updateNotification(millisUntilFinished, durationMillis)
+
+ // Push state to activity
+ stateListener?.onTimerTick(millisUntilFinished, durationMillis, secondsLeft, ringColor)
+ }
+
+ override fun onFinish() {
+ isTimerRunning = false
+ prefs?.edit()?.putBoolean(KEY_TIMER_RUNNING, false)?.apply()
+
+ toneGenerator?.startTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
+ vibrator?.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
+
+ updateNotification(0, durationMillis)
+ stateListener?.onTimerFinished()
+ }
+ }
+ }
+
+ // ---- Notification ----
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Countdown timer progress"
+ lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
+ }
+ notificationManager?.createNotificationChannel(channel)
+ }
+
+ private fun buildNotification(remainingMillis: Long, totalMillis: Long): android.app.Notification {
+ val secondsRemaining = (remainingMillis / 1000).toInt()
+ val minutes = secondsRemaining / 60
+ val seconds = secondsRemaining % 60
+ val timeText = String.format("%02d:%02d", minutes, seconds)
+ val progressPercent = if (totalMillis > 0) {
+ ((1 - remainingMillis.toFloat() / totalMillis) * 100).toInt()
+ } else {
+ 0
+ }
+
+ val stopPendingIntent = PendingIntent.getService(
+ this, 0,
+ Intent(this, CountdownService::class.java).apply { action = ACTION_STOP },
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val restartPendingIntent = PendingIntent.getService(
+ this, 1,
+ Intent(this, CountdownService::class.java).apply { action = ACTION_RESTART },
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val activityPendingIntent = PendingIntent.getActivity(
+ this, 2,
+ Intent(this, MainActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Bong Hit Timer")
+ .setContentText(timeText)
+ .setSmallIcon(R.drawable.ic_timer)
+ .setProgress(100, progressPercent, false)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOngoing(true)
+ .addAction(R.drawable.ic_stop_square, "Stop", stopPendingIntent)
+ .addAction(R.drawable.ic_refresh_square, "Restart", restartPendingIntent)
+ .setContentIntent(activityPendingIntent)
+ .build()
+ }
+
+ private fun updateNotification(remainingMillis: Long, totalMillis: Long) {
+ notificationManager?.notify(NOTIFICATION_ID, buildNotification(remainingMillis, totalMillis))
+ }
+}
diff --git a/app/src/main/java/com/example/countdowntimer/MainActivity.kt b/app/src/main/java/com/example/countdowntimer/MainActivity.kt
index ad82ed0..a2c2ab5 100644
--- a/app/src/main/java/com/example/countdowntimer/MainActivity.kt
+++ b/app/src/main/java/com/example/countdowntimer/MainActivity.kt
@@ -1,8 +1,12 @@
package com.example.countdowntimer
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
import android.content.SharedPreferences
import android.os.Bundle
+import android.os.IBinder
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
@@ -14,12 +18,12 @@ import com.google.android.material.chip.ChipGroup
import android.animation.ValueAnimator
import android.view.animation.AnticipateOvershootInterpolator
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(), TimerStateListener {
companion object {
private const val PREFS_NAME = "timer_prefs"
private const val KEY_SELECTED_DURATION = "selected_duration"
- private const val DEFAULT_DURATION = 10000L
+ private const val DEFAULT_DURATION = 600000L
}
private lateinit var prefs: SharedPreferences
@@ -32,18 +36,31 @@ class MainActivity : AppCompatActivity() {
private var lastSelectedChipId: Int = -1
private var pulseAnimator: ValueAnimator? = null
+ // Service binding
+ private var service: CountdownService? = null
+ private var serviceBound = false
+ private lateinit var serviceConnection: ServiceConnection
+
+ // Countdown UI views
+ private lateinit var cvCountdown: CountdownCanvasView
+ private lateinit var btnStop: View
+ private lateinit var btnRestart: View
+ private lateinit var btnDismiss: Button
+ private lateinit var countdownLayout: View
+ private lateinit var pickerLayout: View
+
private fun durationToChipId(duration: Long): Int = when (duration) {
- 5000L -> R.id.chip5s
- 15000L -> R.id.chip15s
- 30000L -> R.id.chip30s
- else -> R.id.chip10s
+ 300000L -> R.id.chip5m
+ 600000L -> R.id.chip10m
+ 900000L -> R.id.chip15m
+ else -> R.id.chip10m
}
private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
- R.id.chip5s -> 5000L
- R.id.chip10s -> 10000L
- R.id.chip15s -> 15000L
- R.id.chip30s -> 30000L
+ R.id.chip5m -> 300000L
+ R.id.chip10m -> 600000L
+ R.id.chip15m -> 900000L
+ R.id.chip30m -> 1800000L
else -> DEFAULT_DURATION
}
@@ -53,7 +70,6 @@ class MainActivity : AppCompatActivity() {
private fun onChipClicked(chipId: Int) {
if (chipId == lastSelectedChipId) {
- // Re-selection: provide feedback so user knows the selection is registered
vibrator.vibrate(VibrationEffect.createOneShot(15, VibrationEffect.DEFAULT_AMPLITUDE))
val originalText = btnDurationLabel.text
btnDurationLabel.text = "${selectedDuration / 1000}s!"
@@ -85,6 +101,65 @@ class MainActivity : AppCompatActivity() {
btnStart.alpha = 1f
}
+ private fun showCountdownUI() {
+ pickerLayout.visibility = View.GONE
+ countdownLayout.visibility = View.VISIBLE
+ stopPulseAnimation()
+ }
+
+ private fun hideCountdownUI() {
+ countdownLayout.visibility = View.GONE
+ pickerLayout.visibility = View.VISIBLE
+ btnDismiss.visibility = View.GONE
+ btnStop.visibility = View.VISIBLE
+ btnRestart.visibility = View.VISIBLE
+ startPulseAnimation()
+ }
+
+ // ---- ServiceConnection ----
+
+ private fun initServiceConnection() {
+ serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
+ val localBinder = binder as CountdownService.LocalBinder
+ service = localBinder.getService()
+ serviceBound = true
+ service?.setStateListener(this@MainActivity)
+ if (service?.isRunning() == true) {
+ showCountdownUI()
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ serviceBound = false
+ service = null
+ }
+ }
+ }
+
+ // ---- TimerStateListener callbacks ----
+
+ override fun onTimerTick(remainingMillis: Long, totalDuration: Long, secondsLeft: Int, ringColor: Int) {
+ val progress = remainingMillis.toFloat() / totalDuration
+ cvCountdown.setProgress(progress)
+ cvCountdown.setColor(ringColor)
+ cvCountdown.setText(secondsLeft.toString())
+ }
+
+ override fun onTimerFinished() {
+ cvCountdown.setText(getString(R.string.timer_zero))
+ cvCountdown.setProgress(0f)
+ btnStop.visibility = View.GONE
+ btnRestart.visibility = View.GONE
+ btnDismiss.visibility = View.VISIBLE
+ }
+
+ override fun onTimerStopped() {
+ hideCountdownUI()
+ }
+
+ // ---- Activity lifecycle ----
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -92,6 +167,7 @@ class MainActivity : AppCompatActivity() {
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION)
+ // Picker UI views
btnDurationLabel = findViewById(R.id.btnDurationLabel)
hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
chipGroup = findViewById(R.id.chipGroup)
@@ -99,6 +175,14 @@ class MainActivity : AppCompatActivity() {
val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator
+ // Countdown UI views
+ cvCountdown = findViewById(R.id.cvCountdown)
+ btnStop = findViewById(R.id.btnStop)
+ btnRestart = findViewById(R.id.btnRestart)
+ btnDismiss = findViewById(R.id.btnDismiss)
+ countdownLayout = findViewById(R.id.countdownLayout)
+ pickerLayout = findViewById(R.id.pickerLayout)
+
updateDurationLabel()
// Restore selected chip
@@ -122,34 +206,60 @@ class MainActivity : AppCompatActivity() {
val checkedId = if (checkedIds.size > 0) checkedIds[0].toInt() else -1
if (checkedId != -1) {
if (checkedId != lastSelectedChipId) {
- // New selection: update duration, save, and refresh label
selectedDuration = chipIdToDuration(checkedId)
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
updateDurationLabel()
lastSelectedChipId = checkedId
}
- // Always close the panel on any selection
hsvDurationOptions.visibility = View.GONE
}
}
- // Set individual click listeners to detect re-selection
- findViewById(R.id.chip5s).setOnClickListener { onChipClicked(R.id.chip5s) }
- findViewById(R.id.chip10s).setOnClickListener { onChipClicked(R.id.chip10s) }
- findViewById(R.id.chip15s).setOnClickListener { onChipClicked(R.id.chip15s) }
- findViewById(R.id.chip30s).setOnClickListener { onChipClicked(R.id.chip30s) }
+ findViewById(R.id.chip5m).setOnClickListener { onChipClicked(R.id.chip5m) }
+ findViewById(R.id.chip10m).setOnClickListener { onChipClicked(R.id.chip10m) }
+ findViewById(R.id.chip15m).setOnClickListener { onChipClicked(R.id.chip15m) }
+ findViewById(R.id.chip30m).setOnClickListener { onChipClicked(R.id.chip30m) }
- findViewById(R.id.btnStart).setOnClickListener {
- stopPulseAnimation()
- CountdownDialog(this, selectedDuration, prefs).show()
+ // Start button: launch the service instead of showing a dialog
+ btnStart.setOnClickListener {
+ val intent = Intent(this, CountdownService::class.java).apply {
+ action = CountdownService.ACTION_START
+ putExtra(CountdownService.EXTRA_DURATION, selectedDuration)
+ }
+ startService(intent)
+ showCountdownUI()
}
+ // Countdown button handlers
+ btnStop.setOnClickListener { service?.stopTimer() }
+ btnRestart.setOnClickListener { service?.restartTimer() }
+ btnDismiss.setOnClickListener {
+ service?.stopTimer()
+ hideCountdownUI()
+ }
+
+ initServiceConnection()
startPulseAnimation()
}
+ override fun onStart() {
+ super.onStart()
+ Intent(this, CountdownService::class.java).also { intent ->
+ bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (serviceBound) {
+ unbindService(serviceConnection)
+ serviceBound = false
+ }
+ }
+
override fun onResume() {
super.onResume()
- if (!isFinishing && !isDestroyed) {
+ if (!isFinishing && !isDestroyed && pickerLayout.visibility == View.VISIBLE) {
startPulseAnimation()
}
}
@@ -158,4 +268,4 @@ class MainActivity : AppCompatActivity() {
super.onPause()
stopPulseAnimation()
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/countdowntimer/TimerStateListener.kt b/app/src/main/java/com/example/countdowntimer/TimerStateListener.kt
new file mode 100644
index 0000000..2b9e692
--- /dev/null
+++ b/app/src/main/java/com/example/countdowntimer/TimerStateListener.kt
@@ -0,0 +1,7 @@
+package com.example.countdowntimer
+
+interface TimerStateListener {
+ fun onTimerTick(remainingMillis: Long, totalDuration: Long, secondsLeft: Int, ringColor: Int)
+ fun onTimerFinished()
+ fun onTimerStopped()
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index c739343..9063e0b 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -29,221 +29,304 @@
android:layout_height="wrap_content"
android:padding="24dp">
-
-
-
+
+ app:layout_constraintTop_toTopOf="parent">
-
+
-
-
-
-
-
-
+ android:layout_marginTop="16dp"
+ android:gravity="center"
+ android:text="@string/instruction_title"
+ android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
+ android:textStyle="bold"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="24dp"
+ android:orientation="vertical"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/tvInstructionTitle">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:singleSelection="true">
-
+
-
+
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toTopOf="parent">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_countdown.xml b/app/src/main/res/layout/dialog_countdown.xml
deleted file mode 100644
index 0abdf12..0000000
--- a/app/src/main/res/layout/dialog_countdown.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-