Compare commits

..
14 Commits
Author SHA1 Message Date
kfj001 74c822484d string alterations 2026-06-28 08:30:06 +00:00
kfj001 d7ce314c22 migrate timer to ForegroundService for background survival
Move CountDownTimer logic from a Dialog into a ForegroundService so
the timer survives when the user tabs away or the activity is
destroyed. The activity becomes a thin UI projection bound to the
service via TimerStateListener callbacks.

- Create CountdownService with notification, state persistence,
  and ToneGenerator/Vibrator ownership
- Persist timer state to SharedPreferences for process death recovery
- Add notification with progress bar and Stop/Restart actions
- Embed countdown UI in activity_main.xml alongside picker UI
- Delete CountdownDialog and dialog_countdown.xml
- Update AndroidManifest with foreground service permissions
2026-06-23 01:49:11 +00:00
kfj001 da29e51993 stable, alpine based devcontainer 2026-06-22 23:20:44 +00:00
kfj001 7a91ecba18 visual polish: add pulsing start button and color-shifting progress ring
- Countdown ring interpolates green → yellow → red as timer progresses
- Start button pulses continuously to draw attention
- Pulse stops while dialog is shown and restarts on resume
2026-05-30 10:51:36 -07:00
kfj001 97b01437b2 bug fixes 2026-05-29 20:30:41 -07:00
kfj001 f87d23a922 removes unneeded file 2026-05-28 22:59:26 -07:00
kfj001 b04f197ec2 Add devcontainer setup, bump minSdk to 34, rename app to Bong Hit Timer 2026-05-28 22:39:48 -07:00
kfj001andClaude Opus 4.7 634e0ae48a Redesign start button as circular icon with spacing adjustment
Replace the rectangular Material Button with a circular FrameLayout
containing the play icon. Add a small spacer above it for better
visual separation from the duration chips. Update MainActivity to
reference the new View type instead of Button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:20:49 -07:00
kfj001 451569de17 visual enhancements and documentation changes. 2026-05-27 18:32:37 -07:00
kfj001 075311d298 Adjustments to main screen layout to facilitate wider displays. 2026-05-27 13:06:13 -07:00
kfj001 a3835e1404 Simplifies the instructions 2026-05-27 00:25:55 -07:00
kfj001 bc259602aa new art assets from clipart/AI. 2026-05-27 00:20:20 -07:00
kfj001 3dd453e536 Removes additional layout for large format devices - resuing existing layout. 2026-05-26 22:56:04 -07:00
kfj001andClaude Opus 4.7 aca180cf20 add missing gradle-wrapper.jar to fix gradlew
The jar was never committed (caught by *.jar in .gitignore).
Added a specific exception so future wrapper jars stay tracked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:09:59 -07:00
28 changed files with 960 additions and 612 deletions
+33
View File
@@ -0,0 +1,33 @@
FROM ghcr.io/alvr/alpine-android:android-34-jdk21
# Install Android SDK command-line tools
# ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
# ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
# ARG ANDROID_SDK_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
# RUN apk update && apk add curl zip git openjdk21-jdk cmake gcc
# RUN mkdir -p /usr/local/android-sdk/cmdline-tools && \
# cd /tmp && \
# curl -fsSL "$ANDROID_SDK_URL" -o cmdline-tools.zip && \
# unzip -q cmdline-tools.zip -d /usr/local/android-sdk/cmdline-tools && \
# mv /usr/local/android-sdk/cmdline-tools/cmdline-tools /usr/local/android-sdk/cmdline-tools/latest && \
# rm cmdline-tools.zip
# # Set Android SDK paths and environment
# ENV ANDROID_HOME=/usr/local/android-sdk
# ENV ANDROID_COMMAND_LINE_TOOL=/usr/local/android-sdk/cmdline-tools/latest/bin
# ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}
# # Accept licenses and install required SDK packages
# RUN mkdir -p $ANDROID_HOME/.android && \
# touch $ANDROID_HOME/.android/repositories.cfg && \
# yes | ${ANDROID_COMMAND_LINE_TOOL}/sdkmanager --licenses 2>/dev/null || true && \
# ${ANDROID_COMMAND_LINE_TOOL}/sdkmanager \
# "platforms;android-34" \
# "build-tools;33.0.1" \
# "platform-tools" \
# 2>&1 | tee /dev/null
# # Grant execute permission to gradlew
# RUN chmod +x /workspaces/*/gradlew 2>/dev/null || true
+22
View File
@@ -0,0 +1,22 @@
{
"name": "Bong Hit Timer Android",
"dockerFile": "Dockerfile",
"customizations": {
"vscode": {
"extensions": [
"redhat.vscode-xml",
"redhat.vscode-yaml",
"felipecaslazana.vscode-android-logcat",
"ms-azuretools.vscode-docker",
"fwcd.kotlin",
"vscjava.vscode-java-pack",
"Anthropic.claude-code"
]
}
},
"forwardPorts": [
5554,
5555
],
"postCreateCommand": "./gradlew --version"
}
+1
View File
@@ -5,6 +5,7 @@ gradle-app.setting
# native lib outputs # native lib outputs
project-classes/ project-classes/
*.jar *.jar
!gradle/wrapper/gradle-wrapper.jar
*.dex *.dex
# Android Studio # Android Studio
bin/ bin/
+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
+14 -10
View File
@@ -7,25 +7,29 @@
- **Run Unit Tests**: `./gradlew test` (if applicable) - **Run Unit Tests**: `./gradlew test` (if applicable)
## Code Style Guidelines ## Code Style Guidelines
- **Language**: Kotlin 1.9+ - **Language**: Kotlin 1.9.0
- **UI Framework**: Traditional Android View system (XML layouts), **not** Jetpack Compose. - **UI Framework**: Traditional Android View system (XML layouts), **not** Jetpack Compose.
- **Naming Conventions**: - **Naming Conventions**:
- Classes: `PascalCase` (e.g., `CountdownDialog`) - Classes: `PascalCase` (e.g., `CountdownDialog`)
- Functions/Variables: `camelCase` (e.g., `startTimer()`) - Functions/Variables: `camelCase` (e.g., `startTimer()`)
- Layout Files: `snake_case` (e.g., `activity_main.xml`) - Layout Files: `snake_case` (e.g., `activity_main.xml`)
- Resource IDs: `snake_case` (e.g., `btn_start`) - Resource IDs: `camelCase` (e.g., `btnStart`, `cvCountdown`)
- **Android Patterns**: Use `findViewById` for view binding in this project to maintain consistency with existing simple architecture. - **Android Patterns**: Use `findViewById` for view binding in this project to maintain consistency with existing simple architecture.
## Architecture ## Architecture
- **Pattern**: Simple Activity-Dialog architecture. - **Pattern**: Activity-Service architecture with `ForegroundService`.
- **State**: The countdown state is managed locally within `CountdownDialog` using `android.os.CountDownTimer`. - **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. - **Dependency Management**: Gradle with Groovy DSL.
- **Key Constraints**: - **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 ## 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/MainActivity.kt`: Entry point, binds to service, implements `TimerStateListener`.
- `app/src/main/java/com/example/countdowntimer/CountdownDialog.kt`: Implementation of the countdown logic and `ToneGenerator` trigger. - `app/src/main/java/com/example/countdowntimer/CountdownService.kt`: ForegroundService owning timer logic, `ToneGenerator`, `Vibrator`, notification, and state persistence.
- `app/src/main/res/layout/activity_main.xml`: Main screen layout. - `app/src/main/java/com/example/countdowntimer/TimerStateListener.kt`: Callback interface for service-to-activity state updates.
- `app/src/main/res/layout/dialog_countdown.xml`: Timer dialog layout. - `app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt`: Custom view for the circular progress ring and countdown text.
- `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.
+34 -7
View File
@@ -3,12 +3,12 @@
A minimal Android application that demonstrates a simple countdown functionality with an audible alert. A minimal Android application that demonstrates a simple countdown functionality with an audible alert.
## Description ## Description
The Countdown Timer app is a lightweight utility that launches a modal dialog to count down from 10 seconds. Once the timer reaches zero, the app plays a system error tone and closes the dialog. This project serves as a basic example of using `CountDownTimer`, `ToneGenerator`, and custom `Dialog` implementation in Android. The Countdown Timer app is a lightweight utility that launches a modal dialog to count down from a user-selected duration (5s, 10s, 15s, or 30s). Once the timer reaches zero, the app plays a confirmation tone and vibration, then shows a dismiss button. This project serves as a basic example of using `CountDownTimer`, `ToneGenerator`, and custom `Dialog` implementation in Android.
## Key Features ## Key Features
- **Single-Action Interface**: A simple main screen with a start button. - **Selectable Duration**: A main screen with a duration picker (chip group) and a start button.
- **Modal Countdown**: A non-cancelable dialog that prevents user interference during the countdown. - **Modal Countdown**: A full-screen dialog with circular progress ring, stop/restart buttons, and a confirmation tone during the final 3 seconds.
- **Audible Alert**: Integration with Android's `ToneGenerator` to play `TONE_SUP_ERROR` upon completion. - **Haptic Feedback**: Vibration on the last 3 seconds and on completion (configurable via shared preferences).
- **Real-time Updates**: UI updates every second to reflect the remaining time. - **Real-time Updates**: UI updates every second to reflect the remaining time.
## Getting Started ## Getting Started
@@ -25,19 +25,46 @@ To build the debug APK from the command line:
``` ```
### Running the App ### Running the App
#### Via Android Studio
1. Open the project in Android Studio. 1. Open the project in Android Studio.
2. Select your target device/emulator. 2. Select your target device/emulator.
3. Click the **Run** button or press `Shift + F10`. 3. Click the **Run** button or press `Shift + F10`.
#### Via ADB
First, ensure your device or emulator is connected (`adb devices`). Then install the debug APK and launch the app:
```bash
# Install the debug APK
adb install -r app/build/outputs/apk/debug/app-debug.apk
# Launch the app
adb shell am start -n com.example.countdowntimer.debug/com.example.countdowntimer.MainActivity
```
#### Taking a Screenshot via ADB
To capture a screenshot of the app from your host machine:
```bash
# Take the screenshot on the device
adb shell screencap -p /sdcard/screenshot.png
# Pull it to your host machine
adb pull /sdcard/screenshot.png screenshots/
```
## Project Structure ## Project Structure
```text ```text
app/ app/
├── src/ ├── src/
│ └── main/ │ └── main/
│ ├── java/com/example/countdowntimer/ │ ├── java/com/example/countdowntimer/
│ │ ├── MainActivity.kt # Main entry point; launches the timer │ │ ├── MainActivity.kt # Main entry point; duration picker and timer trigger
│ │ ── CountdownDialog.kt # Logic for the 10s countdown and tone │ │ ── CountdownDialog.kt # Countdown logic, tone, and vibration
├── res/ # UI layouts and resources │ └── CountdownCanvasView.kt # Custom circular progress ring view
│ ├── res/layout/ # UI layouts
│ │ ├── activity_main.xml
│ │ └── dialog_countdown.xml
│ └── AndroidManifest.xml # Application manifest │ └── AndroidManifest.xml # Application manifest
└── build.gradle # App-level build configuration └── build.gradle # App-level build configuration
``` ```
+1 -1
View File
@@ -9,7 +9,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.example.countdowntimer" applicationId "com.example.countdowntimer"
minSdk 24 minSdk 34
targetSdk 34 targetSdk 34
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
+8 -1
View File
@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_timer" android:icon="@drawable/ic_timer"
android:label="Countdown Timer" android:label="Bong Hit Timer"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.CountdownTimer"> android:theme="@style/Theme.CountdownTimer">
<activity <activity
@@ -16,5 +18,10 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".CountdownService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </manifest>
@@ -47,6 +47,13 @@ class CountdownCanvasView @JvmOverloads constructor(
invalidate() invalidate()
} }
fun setColor(color: Int) {
if (ringPaint.color != color) {
ringPaint.color = color
invalidate()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
val inset = ringPaint.strokeWidth / 2f + 12f val inset = ringPaint.strokeWidth / 2f + 12f
@@ -1,113 +0,0 @@
package com.example.countdowntimer
import android.app.Dialog
import android.content.Context
import android.content.SharedPreferences
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)
timer = object : CountDownTimer(durationMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
val progress = millisUntilFinished.toFloat() / durationMillis
cvCountdown.setProgress(progress)
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)
}
}
}
@@ -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))
}
}
@@ -1,40 +1,66 @@
package com.example.countdowntimer package com.example.countdowntimer
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.chip.ChipGroup 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 { companion object {
private const val PREFS_NAME = "timer_prefs" private const val PREFS_NAME = "timer_prefs"
private const val KEY_SELECTED_DURATION = "selected_duration" 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 private lateinit var prefs: SharedPreferences
private lateinit var btnDurationLabel: Button private lateinit var btnDurationLabel: Button
private lateinit var hsvDurationOptions: View private lateinit var hsvDurationOptions: View
private lateinit var chipGroup: ChipGroup private lateinit var chipGroup: ChipGroup
private lateinit var btnStart: View
private lateinit var vibrator: Vibrator
private var selectedDuration: Long = DEFAULT_DURATION private var selectedDuration: Long = DEFAULT_DURATION
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) { private fun durationToChipId(duration: Long): Int = when (duration) {
5000L -> R.id.chip5s 300000L -> R.id.chip5m
15000L -> R.id.chip15s 600000L -> R.id.chip10m
30000L -> R.id.chip30s 900000L -> R.id.chip15m
else -> R.id.chip10s else -> R.id.chip10m
} }
private fun chipIdToDuration(chipId: Int): Long = when (chipId) { private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
R.id.chip5s -> 5000L R.id.chip5m -> 300000L
R.id.chip10s -> 10000L R.id.chip10m -> 600000L
R.id.chip15s -> 15000L R.id.chip15m -> 900000L
R.id.chip30s -> 30000L R.id.chip30m -> 1800000L
else -> DEFAULT_DURATION else -> DEFAULT_DURATION
} }
@@ -42,6 +68,98 @@ class MainActivity : AppCompatActivity() {
btnDurationLabel.text = "${selectedDuration / 1000}s" btnDurationLabel.text = "${selectedDuration / 1000}s"
} }
private fun onChipClicked(chipId: Int) {
if (chipId == lastSelectedChipId) {
vibrator.vibrate(VibrationEffect.createOneShot(15, VibrationEffect.DEFAULT_AMPLITUDE))
val originalText = btnDurationLabel.text
btnDurationLabel.text = "${selectedDuration / 1000}s!"
btnDurationLabel.postDelayed({ btnDurationLabel.text = originalText }, 300)
}
}
private fun startPulseAnimation() {
pulseAnimator = ValueAnimator.ofFloat(1f, 0.78f, 1f).apply {
duration = 1500
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
interpolator = AnticipateOvershootInterpolator(1f)
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
btnStart.scaleX = fraction
btnStart.scaleY = fraction
btnStart.alpha = fraction
}
}
pulseAnimator?.start()
}
private fun stopPulseAnimation() {
pulseAnimator?.cancel()
pulseAnimator = null
btnStart.scaleX = 1f
btnStart.scaleY = 1f
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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@@ -49,9 +167,21 @@ class MainActivity : AppCompatActivity() {
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION) selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION)
// Picker UI views
btnDurationLabel = findViewById(R.id.btnDurationLabel) btnDurationLabel = findViewById(R.id.btnDurationLabel)
hsvDurationOptions = findViewById(R.id.hsvDurationOptions) hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
chipGroup = findViewById(R.id.chipGroup) chipGroup = findViewById(R.id.chipGroup)
btnStart = findViewById(R.id.btnStart)
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() updateDurationLabel()
@@ -72,17 +202,70 @@ class MainActivity : AppCompatActivity() {
} }
} }
chipGroup.setOnCheckedChangeListener { _, checkedId -> chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
val checkedId = if (checkedIds.size > 0) checkedIds[0].toInt() else -1
if (checkedId != -1) { if (checkedId != -1) {
if (checkedId != lastSelectedChipId) {
selectedDuration = chipIdToDuration(checkedId) selectedDuration = chipIdToDuration(checkedId)
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply() prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
updateDurationLabel() updateDurationLabel()
lastSelectedChipId = checkedId
}
hsvDurationOptions.visibility = View.GONE hsvDurationOptions.visibility = View.GONE
} }
} }
findViewById<Button>(R.id.btnStart).setOnClickListener { findViewById<View>(R.id.chip5m).setOnClickListener { onChipClicked(R.id.chip5m) }
CountdownDialog(this, selectedDuration, prefs).show() findViewById<View>(R.id.chip10m).setOnClickListener { onChipClicked(R.id.chip10m) }
findViewById<View>(R.id.chip15m).setOnClickListener { onChipClicked(R.id.chip15m) }
findViewById<View>(R.id.chip30m).setOnClickListener { onChipClicked(R.id.chip30m) }
// 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 && pickerLayout.visibility == View.VISIBLE) {
startPulseAnimation()
}
}
override fun onPause() {
super.onPause()
stopPulseAnimation()
}
} }
@@ -0,0 +1,7 @@
package com.example.countdowntimer
interface TimerStateListener {
fun onTimerTick(remainingMillis: Long, totalDuration: Long, secondsLeft: Int, ringColor: Int)
fun onTimerFinished()
fun onTimerStopped()
}
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="#81C784" /> <item android:state_checked="true" android:color="#2E7D32" />
<item android:color="#E8E8E8" /> <item android:color="#233024" />
</selector> </selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="#C8E6C9" />
<item android:color="#2E7D32" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="#FFFFFF" />
<item android:color="#C8E6C9" />
</selector>
+5
View File
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="64dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="64dp">
<path android:fillColor="@android:color/white" android:pathData="M200,840Q183,840 171.5,828.5Q160,817 160,800L160,760L160,760Q110,760 75,725Q40,690 40,640L40,440Q40,390 75,355Q110,320 160,320L160,320L160,240Q160,190 195,155Q230,120 280,120L680,120Q730,120 765,155Q800,190 800,240L800,320L800,320Q850,320 885,355Q920,390 920,440L920,640Q920,690 885,725Q850,760 800,760L800,760L800,800Q800,817 788.5,828.5Q777,840 760,840Q743,840 731.5,828.5Q720,817 720,800L720,760L240,760L240,800Q240,817 228.5,828.5Q217,840 200,840ZM160,680L800,680Q817,680 828.5,668.5Q840,657 840,640L840,440Q840,423 828.5,411.5Q817,400 800,400Q783,400 771.5,411.5Q760,423 760,440L760,600L200,600L200,440Q200,423 188.5,411.5Q177,400 160,400Q143,400 131.5,411.5Q120,423 120,440L120,640Q120,657 131.5,668.5Q143,680 160,680ZM280,520L680,520L680,440Q680,413 691,391Q702,369 720,352L720,240Q720,223 708.5,211.5Q697,200 680,200L280,200Q263,200 251.5,211.5Q240,223 240,240L240,352Q258,369 269,391Q280,413 280,440L280,520ZM480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520L480,520ZM480,680Q480,680 480,680Q480,680 480,680L480,680Q480,680 480,680Q480,680 480,680Q480,680 480,680Q480,680 480,680L480,680L480,680L480,680Q480,680 480,680Q480,680 480,680Q480,680 480,680Q480,680 480,680L480,680Q480,680 480,680Q480,680 480,680ZM480,600Q480,600 480,600Q480,600 480,600L480,600L480,600L480,600Q480,600 480,600Q480,600 480,600L480,600L480,600Q480,600 480,600Q480,600 480,600L480,600L480,600L480,600Q480,600 480,600Q480,600 480,600L480,600Z"/>
</vector>
@@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:pathData="M20,150A80,80 0,1 1,180 150"
android:strokeWidth="20"
android:fillColor="#00000000"
android:strokeColor="#e0e0e0"
android:strokeLineCap="round"/>
<path
android:pathData="M20,150A80,80 0,1 1,180 150"
android:strokeWidth="20"
android:fillColor="#00000000"
android:strokeColor="#4caf50"
android:strokeLineCap="round"/>
<path
android:fillColor="#FF000000"
android:pathData="M100,150L155,70"
android:strokeWidth="12"
android:strokeColor="#333"
android:strokeLineCap="round"/>
<path
android:pathData="M100,150m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
android:fillColor="#333"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="120"
android:viewportHeight="120"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M0,20v80l80,-40z" />
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M460,800Q410,800 375,765Q340,730 340,680L420,680Q420,697 431.5,708.5Q443,720 460,720Q477,720 488.5,708.5Q500,697 500,680Q500,663 488.5,651.5Q477,640 460,640L80,640L80,560L460,560Q510,560 545,595Q580,630 580,680Q580,730 545,765Q510,800 460,800ZM80,400L80,320L620,320Q646,320 663,303Q680,286 680,260Q680,234 663,217Q646,200 620,200Q594,200 577,217Q560,234 560,260L480,260Q480,201 520.5,160.5Q561,120 620,120Q679,120 719.5,160.5Q760,201 760,260Q760,319 719.5,359.5Q679,400 620,400L80,400ZM740,720L740,640Q766,640 783,623Q800,606 800,580Q800,554 783,537Q766,520 740,520L80,520L80,440L740,440Q799,440 839.5,480.5Q880,521 880,580Q880,639 839.5,679.5Q799,720 740,720Z"/>
</vector>
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="64dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="64dp">
<path android:fillColor="@android:color/white" android:pathData="M655,880L513,738L569,682L654,767L824,597L880,654L655,880ZM480,490L480,490L480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490Q480,490 480,490ZM412,750L501,839Q496,839 490.5,839.5Q485,840 480,840Q346,840 253,747Q160,654 160,520Q160,391 246.5,275Q333,159 480,80L480,212Q480,246 503.5,269Q527,292 561,292Q579,292 594.5,284.5Q610,277 622,262L640,240Q714,282 757,357Q800,432 800,520L720,520Q720,495 715,471.5Q710,448 701,425.5Q692,403 678.5,383Q665,363 648,346Q628,359 606,365.5Q584,372 561,372Q499,372 453.5,331Q408,290 401,230L401,230Q323,296 281.5,370.5Q240,445 240,520Q240,602 288.5,665Q337,728 412,750Z"/>
</vector>
@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_main_gradient">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="24dp"
app:cardElevation="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="24dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_timer"
app:tint="@color/color_icon_tint" />
<TextView
android:id="@+id/tvInstructionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/instruction_title"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="32dp"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_volume_up"
app:tint="@color/color_icon_tint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/instruction_volume"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_chair"
app:tint="@color/color_icon_tint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/instruction_position"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_no_distractions"
app:tint="@color/color_icon_tint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/instruction_distractions"
android:textSize="16sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStart"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/start_timer"
app:cornerRadius="20dp"
app:icon="@drawable/ic_play_arrow" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
+174 -40
View File
@@ -14,55 +14,78 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardCornerRadius="24dp" app:cardCornerRadius="24dp"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent=".85">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp"> android:padding="24dp">
<!-- Picker UI -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pickerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView <ImageView
android:layout_width="64dp" android:id="@+id/timerIcon"
android:layout_height="64dp" android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_timer" android:src="@drawable/ic_timer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/color_icon_tint" /> app:tint="@color/color_icon_tint" />
<TextView <TextView
android:id="@+id/tvInstructionTitle" android:id="@+id/tvInstructionTitle"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/instruction_title" android:text="@string/instruction_title"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" /> android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
<LinearLayout <LinearLayout
android:id="@+id/instructionsLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:orientation="vertical"> android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvInstructionTitle">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="8dp"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/ic_volume_up" android:src="@drawable/outline_fire_check_24"
app:tint="@color/color_icon_tint" /> app:tint="@color/color_icon_tint" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -72,16 +95,18 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="8dp"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/ic_chair" android:src="@drawable/outline_air_24"
app:tint="@color/color_icon_tint" /> app:tint="@color/color_icon_tint" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -91,16 +116,18 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="8dp"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/ic_no_distractions" android:src="@drawable/ic_play_arrow"
app:tint="@color/color_icon_tint" /> app:tint="@color/color_icon_tint" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -116,16 +143,19 @@
android:layout_height="80dp" android:layout_height="80dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:background="@drawable/btn_duration_selector" android:background="@drawable/btn_duration_selector"
android:text="@string/start_timer"
android:drawableEnd="@drawable/ic_keyboard_arrow_down" android:drawableEnd="@drawable/ic_keyboard_arrow_down"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:gravity="center"
android:includeFontPadding="false"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:text="@string/start_timer"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" app:layout_constraintEnd_toEndOf="parent"
android:includeFontPadding="false" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/instructionsLayout" />
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/hsvDurationOptions" android:id="@+id/hsvDurationOptions"
@@ -133,7 +163,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:scrollbars="none" android:scrollbars="none"
android:visibility="gone"> android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnDurationLabel">
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup" android:id="@+id/chipGroup"
@@ -142,59 +175,160 @@
app:singleSelection="true"> app:singleSelection="true">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip5s" android:id="@+id/chip5m"
style="@style/Widget.Material3.Chip.Suggestion.Elevated" style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="56dp" android:minWidth="56dp"
android:text="5m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg" app:chipBackgroundColor="@color/chip_duration_bg"
android:text="5s" /> app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip10s" android:id="@+id/chip10m"
style="@style/Widget.Material3.Chip.Suggestion.Elevated" style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="56dp" android:minWidth="56dp"
android:checked="true" android:text="10m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg" app:chipBackgroundColor="@color/chip_duration_bg"
android:text="10s" /> app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip15s" android:id="@+id/chip15m"
style="@style/Widget.Material3.Chip.Suggestion.Elevated" style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="56dp" android:minWidth="56dp"
android:text="15m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg" app:chipBackgroundColor="@color/chip_duration_bg"
android:text="15s" /> app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip30s" android:id="@+id/chip30m"
style="@style/Widget.Material3.Chip.Suggestion.Elevated" style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="56dp" android:minWidth="56dp"
android:text="30m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg" app:chipBackgroundColor="@color/chip_duration_bg"
android:text="30s" /> app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp" />
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>
<Button <View
android:id="@+id/spacerBelowChips"
android:layout_width="1dp"
android:layout_height="5dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hsvDurationOptions" />
<FrameLayout
android:id="@+id/btnStart" android:id="@+id/btnStart"
android:layout_width="120dp" android:layout_width="100dp"
android:layout_height="120dp" android:layout_height="100dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="@drawable/btn_start_selector" android:background="@drawable/btn_start_selector"
android:drawableTop="@drawable/ic_play_arrow" android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spacerBelowChips">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:contentDescription="Start"
android:padding="20dp"
android:src="@drawable/ic_play_arrow"
android:tint="@android:color/white" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Countdown UI (initially hidden) -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/countdownLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.countdowntimer.CountdownCanvasView
android:id="@+id/cvCountdown"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.9" />
<LinearLayout
android:id="@+id/buttonRow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:gravity="center" android:gravity="center"
android:padding="16dp" android:orientation="horizontal"
android:text="@string/start_timer" app:layout_constraintBottom_toBottomOf="parent"
android:textColor="#FFFFFF" app:layout_constraintEnd_toEndOf="parent"
android:textSize="16sp" app:layout_constraintStart_toStartOf="parent"
android:textStyle="bold" /> app:layout_constraintTop_toBottomOf="@+id/cvCountdown">
<FrameLayout
android:id="@+id/btnStop"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
android:background="@drawable/btn_stop_selector"
android:clickable="true"
android:focusable="true" />
<FrameLayout
android:id="@+id/btnRestart"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/btn_restart_selector"
android:clickable="true"
android:focusable="true" />
</LinearLayout> </LinearLayout>
<Button
android:id="@+id/btnDismiss"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="24dp"
android:background="@drawable/btn_duration_selector"
android:text="Dismiss"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cvCountdown" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>
@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000">
<com.example.countdowntimer.CountdownCanvasView
android:id="@+id/cvCountdown"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonRow"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/buttonRow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<FrameLayout
android:id="@+id/btnStop"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginEnd="16dp"
android:background="@drawable/btn_stop_selector"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:contentDescription="@string/stop_timer"
android:src="@drawable/ic_stop_square" />
</FrameLayout>
<FrameLayout
android:id="@+id/btnRestart"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:background="@drawable/btn_restart_selector"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:contentDescription="@string/restart_timer"
android:src="@drawable/ic_refresh_square" />
</FrameLayout>
</LinearLayout>
<Button
android:id="@+id/btnDismiss"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/btn_duration_selector"
android:gravity="center"
android:padding="16dp"
android:text="Dismiss"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+5 -5
View File
@@ -1,12 +1,12 @@
<resources> <resources>
<string name="app_name">Countdown Timer</string> <string name="app_name">Countdown Timer</string>
<string name="start_timer">Start Timer</string> <string name="start_timer">Start Timer</string>
<string name="countdown_text">!!! HOLD !!!</string> <string name="countdown_text">0</string>
<string name="timer_zero">EXHALE!</string> <string name="timer_zero">AGAIN!</string>
<string name="instruction_title">BONG HIT TIMER!</string> <string name="instruction_title">BONG HIT TIMER!</string>
<string name="instruction_volume">Ensure your bong is loaded</string> <string name="instruction_volume">When the timer elapses</string>
<string name="instruction_position">Find a comfortable position</string> <string name="instruction_position">Take your hit</string>
<string name="instruction_distractions">Light your bowl and take your hit before you press the Start Timer button. HOLD YOUR HIT IN!</string> <string name="instruction_distractions">Press Start to start the countdown to a bong load!</string>
<string name="pick_duration">Pick Duration</string> <string name="pick_duration">Pick Duration</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="vibration">Vibration</string> <string name="vibration">Vibration</string>
+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<!-- Gauge background -->
<path d="M 20 150 A 80 80 0 1 1 180 150"
fill="none" stroke="#e0e0e0" stroke-width="20" stroke-linecap="round"/>
<!-- Full fill -->
<path d="M 20 150 A 80 80 0 1 1 180 150"
fill="none" stroke="#4caf50" stroke-width="20" stroke-linecap="round"
stroke-dasharray="251.2" stroke-dashoffset="0"/>
<!-- Needle -->
<line x1="100" y1="150" x2="155" y2="70" stroke="#333" stroke-width="12" stroke-linecap="round"/>
<circle cx="100" cy="150" r="8" fill="#333"/>
<!-- Label -->
<text x="100" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#4caf50">FULL</text>
</svg>

After

Width:  |  Height:  |  Size: 769 B

-88
View File
@@ -1,88 +0,0 @@
# Goal: Redesign all buttons into bold, square, visually striking interactive elements
## Objective
Replace every basic/rectangular button in the app with larger, square-aspect-ratio buttons that have eye-catching visual design. The goal is to transform the UI from a generic Material 3 look into a distinctive, personality-rich interface that matches the playful "BONG HIT TIMER" brand.
## Current State
The app uses 4 visible buttons across 2 screens:
**MainActivity (home screen):**
- `btnDurationLabel` — outlined button showing selected duration (e.g. "10s") with a dropdown arrow. Uses `Widget.Material3.Button.OutlinedButton` with 20dp corners.
- `btnStart` — filled button labeled "Start Timer" with a play icon. Uses `Widget.Material3.Button` with 20dp corners.
**CountdownDialog (timer screen):**
- `btnStop` — plain Android `Button` labeled "Stop" (default styling, small)
- `btnRestart` — plain Android `Button` labeled "Restart" (default styling, small)
- `btnDismiss` — plain Android `Button` labeled "Dismiss" (currently gone, shows after timer completes)
## Design Requirements
### 1. Square Aspect Ratio
- Change all buttons from `wrap_content` rectangles to a fixed square size (e.g., `120dp` x `120dp` or `100dp` x `100dp`)
- Use `app:layout_constraintDimensionRatio="1:1"` where dynamic sizing is needed
- Buttons should feel substantial and tappable, not compact
### 2. Visual Interest — Each button should have its own personality
Create custom drawable backgrounds for each button type. Design ideas (pick the best approach):
- **Circular/spherical buttons** — Buttons that look like physical 3D balls or pucks. Use gradient fills with highlights/shadows to create a convex "pressable" look.
- **Neon/glow effect** — Since the app is a bong hit timer, a subtle neon glow that pulses or intensifies during countdown would be thematically appropriate.
- **Retro/gaming arcade style** — Bold colors, thick borders, pressed/active states that simulate physical button depression.
- **Gradient + icon centering** — For the timer screen, center the stop/restart icons prominently in the square buttons with bold gradient backgrounds.
### 3. Interactive States
Every button must have rich feedback:
- **Pressed/active state** — noticeable visual change (shrink, darker shade, or inner glow)
- **Default state** — visually engaging with gradients, shadows, or subtle animations
- **Disabled state** — reduced opacity but still recognizable
### 4. Icon Integration
Square buttons are perfect for icon-first design:
- **Stop button** — large centered pause/stop icon
- **Restart button** — large centered refresh/replay icon
- **Start button** — center the play icon, consider removing the text label since the icon is universal
- **Duration label** — consider a pill-shaped or circular chip that pulses when tapped
### 5. Color Palette
Stay within the existing green theme (`#2E7D32` primary, `#4C6842` secondary, `#E8F5E9`/`#C8E6C9` gradient) but push it further:
- Use the primary green for the start/positive actions
- A red/coral accent for stop
- A neutral or blue for restart/duration selection
- Consider gradient transitions within each button (e.g., green to darker green top-to-bottom for a 3D effect)
### 6. Shadow & Elevation
- Add drop shadows to all buttons for depth (use `app:strokeWidth="0dp"` and custom `rippleColor` for Material buttons)
- Create a `btn_square_default.xml` and `btn_square_pressed.xml` in `res/drawable/` for each button type
- The pressed state should visually simulate "pressing in" (reduced shadow, lighter/darker fill)
## Technical Implementation
### New drawable files to create (in `res/drawable/`):
- `btn_start_square.xml` — gradient + shadow drawable for start button
- `btn_start_square_pressed.xml` — pressed state variant
- `btn_stop_square.xml` — gradient + shadow drawable for stop button
- `btn_stop_square_pressed.xml` — pressed state variant
- `btn_restart_square.xml` — gradient + shadow drawable for restart button
- `btn_restart_square_pressed.xml` — pressed state variant
- `btn_duration_chip.xml` — drawable for duration selector
### Layout changes needed:
- `activity_main.xml`:
- `btnDurationLabel`: change to square dimensions, wrap text/icon in center, apply new drawable
- `btnStart`: change to square, center play icon, apply new drawable
- `dialog_countdown.xml`:
- `btnStop`: change from `Button` to `com.google.android.material.button.MaterialButton`, set square dimensions, apply drawable
- `btnRestart`: same treatment
- `btnDismiss`: same treatment, keep as gone by default
- Adjust the ConstraintLayout chain to accommodate square buttons (may need a horizontal chain with proper spacing)
### Code changes needed:
- `CountdownDialog.kt`: Set button backgrounds programmatically if needed, ensure clickable/pressable states work
- No changes to button click logic — only visual overhaul
## Constraints
- Use only Material 3 components already in the dependency (`com.google.android.material:material:1.11.0`)
- Traditional XML layouts — no Compose
- Minimum SDK 24 — no API 30+ features
- Keep the app's existing green aesthetic but make it bolder
- Maintain accessibility (minimum 48dp touch target, sufficient contrast)
Binary file not shown.