Compare commits

...
36 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
kfj001 b30427d862 Merge pull request 'Fat square interesting buttons' (#3) from fat_square_interesting_buttons into main
Reviewed-on: http://192.168.1.74:30008/kfj001/bong_hit_timer_android/pulls/3
2026-05-26 13:24:59 -07:00
kfj001 eaaeb7fa35 fixes buttons on countdown screen 2026-05-26 13:24:08 -07:00
kfj001 6ba2f2cfdf fixes beeping to 3.2.1 cycle 2026-05-26 12:31:28 -07:00
kfj001andClaude Opus 4.7 51c7fa1464 Theme timer ring colors green instead of hardcoded purple
CountdownCanvasView ring progress and track now use themed color
resources (md_color_ring_progress/md_color_ring_track) instead of
hardcoded Material3 purple, so they stay green in both light and dark mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:52:50 -07:00
kfj001andClaude Opus 4.7 df9d92f116 Fix chip selection color green with correct color selector approach
- Replace broken chipBackground theme style with a color state-list
  selector (res/color/chip_duration_bg.xml) using chipBackgroundColor
  attribute directly on each Chip
- Green (#81C784) when checked, light gray (#E8E8E8) when unchecked
- Add minWidth="56dp" to give chips proper width

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:40:32 -07:00
kfj001 5b517f39d6 Merge branch 'worktree-soft-floating-stroustrup' into fat_square_interesting_buttons 2026-05-26 11:24:10 -07:00
kfj001andClaude Opus 4.7 b20208fb40 Make chip selection background green to match app theme
Add custom chip_duration_bg selector drawable (green when checked,
light gray when unchecked) and wire it through a ChipDuration chipStyle
in the theme. This overrides Material3's default purple checked state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:24:10 -07:00
kfj001 9b733f2102 Merge branch 'worktree-soft-floating-stroustrup' into fat_square_interesting_buttons 2026-05-26 11:12:58 -07:00
kfj001andClaude Opus 4.7 0738fc466e Make chip selection color green to match app theme
Add colorPrimaryContainer and set it to the green shade used throughout the app.
This fixes the purplish checked state on Material3 suggestion chips.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:12:58 -07:00
kfj001 7491083e6a Merge branch 'worktree-soft-floating-stroustrup' into fat_square_interesting_buttons 2026-05-26 11:04:36 -07:00
kfj001andClaude Opus 4.7 13786f6c8e Make dropdown arrow white for better visibility on green button
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:04:35 -07:00
kfj001andClaude Opus 4.7 92952a2cf2 Convert timeout selector button from oval to rectangular pill shape
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:50:44 -07:00
kfj001 6de400753c Rounded and larger button treatment 2026-05-26 03:39:25 -07:00
kfj001 822d6fc1b1 fixes import 2026-05-26 03:01:11 -07:00
kfj001 b78b2109a7 Merge branch 'green_motif' of http://192.168.1.74:30008/kfj001/bong_hit_timer_android 2026-05-26 03:00:15 -07:00
kfj001 2701bd22dd fixes import issue 2026-05-26 02:39:53 -07:00
kfj001 1273be03f2 Fixes a crash when changing orientation 2026-05-26 02:39:53 -07:00
kfj001 cb5c9dc547 bug fixes to AI suggested enhancements 2026-05-26 02:39:53 -07:00
kfj001 e00de3d340 added progress ring, vibration and selectable duration (bugged) 2026-05-26 02:39:53 -07:00
kfj001 b5442a6f7f Merge pull request 'Qwen suggestions' (#1) from qwen_suggestions into main
Reviewed-on: http://192.168.1.74:30008/kfj001/bong_hit_timer_android/pulls/1
2026-05-26 02:32:50 -07:00
kfj001 41721bdc04 Fixes a crash when changing orientation 2026-05-26 02:32:18 -07:00
kfj001 79afe50955 Introduces a green motif for light/dark mode. 2026-05-26 02:26:27 -07:00
50 changed files with 1208 additions and 500 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"
+9 -1
View File
@@ -1,19 +1,27 @@
<?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
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<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>
@@ -1,10 +1,11 @@
package com.example.countdowntimer package com.example.countdowntimer
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.RectF
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
@@ -17,18 +18,18 @@ class CountdownCanvasView @JvmOverloads constructor(
private var textToDraw: String = "" private var textToDraw: String = ""
private var progressFraction: Float = 1.0f private var progressFraction: Float = 1.0f
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#6750A4") color = ContextCompat.getColor(context, R.color.md_theme_light_primary)
textSize = 180f textSize = 180f // Roughly equivalent to 72sp depending on density
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
isFakeBoldText = false isFakeBoldText = false
} }
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#6750A4") color = ContextCompat.getColor(context, R.color.md_color_ring_progress)
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 16f strokeWidth = 16f
} }
private val ringBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val ringBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#D0BCFF") color = ContextCompat.getColor(context, R.color.md_color_ring_track)
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 16f strokeWidth = 16f
} }
@@ -46,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,117 +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: Button
private lateinit var btnRestart: Button
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)
if (millisUntilFinished >= durationMillis) {
cvCountdown.setText(context.getString(R.string.countdown_text))
} else {
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
cvCountdown.setText(secondsLeft.toString())
}
if (millisUntilFinished <= 3000 && millisUntilFinished > 1000) {
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()
}
@@ -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="#2E7D32" />
<item android:color="#233024" />
</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>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="40dp" />
<gradient
android:angle="270"
android:startColor="#81C784"
android:endColor="#4CAF50"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#A5D6A7" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="40dp" />
<gradient
android:angle="270"
android:startColor="#C8E6C9"
android:endColor="#A5D6A7"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#E8F5E9" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="40dp" />
<gradient
android:angle="270"
android:startColor="#66BB6A"
android:endColor="#388E3C"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#81C784" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/btn_duration_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/btn_duration_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/btn_duration_pressed" android:state_activated="true" />
<item android:drawable="@drawable/btn_duration_default" />
</selector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#42A5F5"
android:endColor="#1565C0"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#90CAF9" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#90CAF9"
android:endColor="#64B5F6"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#BBDEFB" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#1E88E5"
android:endColor="#0D47A1"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#42A5F5" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/btn_restart_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/btn_restart_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/btn_restart_pressed" android:state_activated="true" />
<item android:drawable="@drawable/btn_restart_default" />
</selector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#4CAF50"
android:endColor="#2E7D32"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#66BB6A" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#A5D6A7"
android:endColor="#81C784"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#C8E6C9" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#388E3C"
android:endColor="#1B5E20"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#4CAF50" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/btn_start_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/btn_start_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/btn_start_pressed" android:state_activated="true" />
<item android:drawable="@drawable/btn_start_default" />
</selector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#EF5350"
android:endColor="#C62828"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#EF9A9A" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#EF9A9A"
android:endColor="#E57373"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#FFCDD2" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="120dp" android:height="120dp" />
<gradient
android:angle="270"
android:startColor="#E53935"
android:endColor="#B71C1C"
android:type="linear" />
<stroke
android:width="3dp"
android:color="#EF5350" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/btn_stop_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/btn_stop_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/btn_stop_pressed" android:state_activated="true" />
<item android:drawable="@drawable/btn_stop_default" />
</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>
@@ -4,6 +4,6 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#6750A4" android:fillColor="#FFFFFF"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6z"/> android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6z"/>
</vector> </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,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4L17.65,6.35z"
android:strokeWidth="0" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M6,6h12v12H6z"
android:strokeWidth="0" />
</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>
+193 -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"
@@ -110,15 +137,25 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<com.google.android.material.button.MaterialButton <Button
android:id="@+id/btnDurationLabel" android:id="@+id/btnDurationLabel"
style="@style/Widget.Material3.Button.OutlinedButton" android:layout_width="180dp"
android:layout_width="wrap_content" android:layout_height="80dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:text="@string/start_timer" android:background="@drawable/btn_duration_selector"
android:drawableEnd="@drawable/ic_keyboard_arrow_down" android:drawableEnd="@drawable/ic_keyboard_arrow_down"
app:cornerRadius="20dp" /> android:drawablePadding="8dp"
android:gravity="center"
android:includeFontPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/start_timer"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/instructionsLayout" />
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/hsvDurationOptions" android:id="@+id/hsvDurationOptions"
@@ -126,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"
@@ -135,47 +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:text="5s" /> android:minWidth="56dp"
android:text="5m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg"
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:checked="true" android:minWidth="56dp"
android:text="10s" /> android:text="10m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg"
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:text="15s" /> android:minWidth="56dp"
android:text="15m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg"
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:text="30s" /> android:minWidth="56dp"
android:text="30m"
android:textColor="@color/chip_duration_text"
app:chipBackgroundColor="@color/chip_duration_bg"
app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp" />
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>
<com.google.android.material.button.MaterialButton <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"
style="@style/Widget.Material3.Button" android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="16dp"
android:background="@drawable/btn_start_selector"
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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="24dp"
android:text="@string/start_timer" android:layout_marginBottom="24dp"
app:cornerRadius="20dp" android:gravity="center"
app:icon="@drawable/ic_play_arrow" /> android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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,51 +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">
<Button
android:id="@+id/btnDismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dismiss"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.example.countdowntimer.CountdownCanvasView
android:id="@+id/cvCountdown"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/btnStop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="16dp" />
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnRestart"
app:layout_constraintHorizontal_chainStyle="spread" />
<Button
android:id="@+id/btnRestart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Restart"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnStop"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+4 -2
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="color_gradient_start">#2E1A47</color> <color name="color_gradient_start">#1B3A1B</color>
<color name="color_gradient_end">#1B0B2E</color> <color name="color_gradient_end">#0D1F0D</color>
<color name="md_color_ring_progress">#A5D6A7</color>
<color name="md_color_ring_track">#2E7D32</color>
</resources> </resources>
+6 -2
View File
@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="md_theme_dark_primary">#A5D6A7</color>
<color name="md_theme_dark_primaryContainer">#2E7D32</color>
<color name="md_theme_dark_secondary">#869F7C</color>
<style name="Theme.CountdownTimer" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.CountdownTimer" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item> <item name="colorPrimary">@color/md_theme_dark_primary</item>
<item name="colorSecondary">@color/md_theme_light_secondary</item> <item name="colorSecondary">@color/md_theme_dark_secondary</item>
<item name="android:statusBarColor">@color/color_gradient_start</item> <item name="android:statusBarColor">@color/color_gradient_start</item>
<item name="android:windowLightStatusBar">false</item> <item name="android:windowLightStatusBar">false</item>
</style> </style>
+5 -3
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="color_gradient_start">#F3E5F5</color> <color name="color_gradient_start">#E8F5E9</color>
<color name="color_gradient_end">#E1BEE7</color> <color name="color_gradient_end">#C8E6C9</color>
<color name="color_icon_tint">#6750A4</color> <color name="color_icon_tint">#2E7D32</color>
<color name="md_color_ring_progress">#2E7D32</color>
<color name="md_color_ring_track">#81C784</color>
</resources> </resources>
+7 -5
View File
@@ -1,14 +1,16 @@
<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>
<string name="sounds">Sounds</string> <string name="sounds">Sounds</string>
<string name="stop_timer">Stop Timer</string>
<string name="restart_timer">Restart Timer</string>
</resources> </resources>
+5 -3
View File
@@ -1,12 +1,14 @@
<resources> <resources>
<style name="Theme.CountdownTimer" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.CountdownTimer" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item> <item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
<item name="colorSecondary">@color/md_theme_light_secondary</item> <item name="colorSecondary">@color/md_theme_light_secondary</item>
<item name="android:statusBarColor">@color/color_gradient_start</item> <item name="android:statusBarColor">@color/color_gradient_start</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
</style> </style>
<color name="md_theme_light_primary">#6750A4</color> <color name="md_theme_light_primary">#2E7D32</color>
<color name="md_theme_light_secondary">#625B71</color> <color name="md_theme_light_primaryContainer">#81C784</color>
<color name="md_theme_light_surface">#FEF7FF</color> <color name="md_theme_light_secondary">#4C6842</color>
<color name="md_theme_light_surface">#F1F8E9</color>
</resources> </resources>
+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

Binary file not shown.