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" /> - - - - - - - - - - - - - - -