commit be631985cb92786530cc7d3e9364f346942ae666 Author: kfj001 Date: Tue May 26 00:51:07 2026 -0700 initial commit diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8a692c7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(*)" + ] + } +} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6df644f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(printenv ANDROID_HOME)", + "Bash(./gradlew assembleDebug *)", + "Bash(export JAVA_HOME=\"/Applications/Android Studio.app/Contents/jbr/Contents/Home\")", + "WebSearch" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b6507e --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Gradle +.gradle/ +gradle-app.setting +!/debug/keep +# native lib outputs +project-classes/ +*.jar +*.dex +# Android Studio +bin/ +gen/ +out/ +# .idea/ is tracked; Android Studio project files are too +.idea/ +*.iml +*.ipr +*.iws + +# Android Build +app/build/ +build/ +capture.log +local.properties + +# Gradle wrapper binaries (pre-downloaded) +gradle-*-bin.zip +gradle-*-src.zip +gradle-*-javadoc.zip + +# Android/Debug +# Build +*.apk +*.ap_ +*.aar +*..aar + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Keystore (if any) +*.jks +*.keystore + +# Log files +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ac81a92 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,31 @@ +# CLAUDE.md - Countdown Timer + +## Build & Test Commands +- **Build Debug APK**: `./gradlew assembleDebug` +- **Build Release APK**: `./gradlew assembleRelease` +- **Clean Project**: `./gradlew clean` +- **Run Unit Tests**: `./gradlew test` (if applicable) + +## Code Style Guidelines +- **Language**: Kotlin 1.9+ +- **UI Framework**: Traditional Android View system (XML layouts), **not** Jetpack Compose. +- **Naming Conventions**: + - Classes: `PascalCase` (e.g., `CountdownDialog`) + - Functions/Variables: `camelCase` (e.g., `startTimer()`) + - Layout Files: `snake_case` (e.g., `activity_main.xml`) + - Resource IDs: `snake_case` (e.g., `btn_start`) +- **Android Patterns**: Use `findViewById` for view binding in this project to maintain consistency with existing simple architecture. + +## Architecture +- **Pattern**: Simple Activity-Dialog architecture. +- **State**: The countdown state is managed locally within `CountdownDialog` using `android.os.CountDownTimer`. +- **Dependency Management**: Gradle with Groovy DSL. +- **Key Constraints**: + - The `CountdownDialog` is explicitly set to `setCancelable(false)` and `setCanceledOnTouchOutside(false)` to ensure the timer completes. + +## Critical Files Reference +- `app/src/main/java/com/example/countdowntimer/MainActivity.kt`: Entry point and trigger for the timer. +- `app/src/main/java/com/example/countdowntimer/CountdownDialog.kt`: Implementation of the countdown logic and `ToneGenerator` trigger. +- `app/src/main/res/layout/activity_main.xml`: Main screen layout. +- `app/src/main/res/layout/dialog_countdown.xml`: Timer dialog layout. +- `app/build.gradle`: Build configuration, SDK versions (minSdk 24, targetSdk 34), and dependencies. diff --git a/CountdownApp/gradle.properties b/CountdownApp/gradle.properties new file mode 100644 index 0000000..646c51b --- /dev/null +++ b/CountdownApp/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..06ea7a4 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Countdown Timer + +A minimal Android application that demonstrates a simple countdown functionality with an audible alert. + +## 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. + +## Key Features +- **Single-Action Interface**: A simple main screen with a start button. +- **Modal Countdown**: A non-cancelable dialog that prevents user interference during the countdown. +- **Audible Alert**: Integration with Android's `ToneGenerator` to play `TONE_SUP_ERROR` upon completion. +- **Real-time Updates**: UI updates every second to reflect the remaining time. + +## Getting Started + +### Prerequisites +- Android Studio (Ladybug or newer recommended) +- JDK 17 +- An Android device or emulator running API level 24 or higher. + +### Building the App +To build the debug APK from the command line: +```bash +./gradlew assembleDebug +``` + +### Running the App +1. Open the project in Android Studio. +2. Select your target device/emulator. +3. Click the **Run** button or press `Shift + F10`. + +## Project Structure +```text +app/ +├── src/ +│ └── main/ +│ ├── java/com/example/countdowntimer/ +│ │ ├── MainActivity.kt # Main entry point; launches the timer +│ │ └── CountdownDialog.kt # Logic for the 10s countdown and tone +│ ├── res/ # UI layouts and resources +│ └── AndroidManifest.xml # Application manifest +└── build.gradle # App-level build configuration +``` diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..57d73ce --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + namespace 'com.example.countdowntimer' + compileSdk 34 + + defaultConfig { + applicationId "com.example.countdowntimer" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + applicationIdSuffix ".debug" + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4cdc27a --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt b/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt new file mode 100644 index 0000000..500aa50 --- /dev/null +++ b/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt @@ -0,0 +1,42 @@ +package com.example.countdowntimer + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View + +class CountdownCanvasView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var textToDraw: String = "" + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#6750A4") // Using the same primary purple + textSize = 180f // Roughly equivalent to 72sp depending on density + textAlign = Paint.Align.CENTER + isFakeBoldText = false + } + + fun setText(text: String) { + if (textToDraw != text) { + textToDraw = text + invalidate() + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (textToDraw.isEmpty()) return + + val fontMetrics = paint.fontMetrics + val x = width / 2f + val y = (height / 2f) - ((fontMetrics.descent + fontMetrics.ascent) / 2f) + + canvas.drawText(textToDraw, x, y, paint) + } +} diff --git a/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt b/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt new file mode 100644 index 0000000..cc85e77 --- /dev/null +++ b/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt @@ -0,0 +1,97 @@ +package com.example.countdowntimer + +import android.app.Dialog +import android.content.Context +import android.media.AudioManager +import android.media.ToneGenerator +import android.os.Bundle +import android.os.CountDownTimer +import android.view.View +import android.view.WindowManager +import android.widget.Button + +class CountdownDialog(private val context: Context, private val durationMillis: Long) : 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 + + 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) + + btnStop.setOnClickListener { cancelTimer() } + btnRestart.setOnClickListener { restartTimer() } + btnDismiss.setOnClickListener { dismiss() } + + startTimer() + } + + private fun startTimer() { + timer = object : CountDownTimer(durationMillis, 1000) { + override fun onTick(millisUntilFinished: Long) { + val secondsLeft = (millisUntilFinished / 1000).toInt() + 1 + if (secondsLeft < 10) { + cvCountdown.setText(secondsLeft.toString()) + } else { + cvCountdown.setText(context.getString(R.string.countdown_text)) + } + if (secondsLeft in 1..3) { + playTone(ToneGenerator.TONE_PROP_BEEP) + } + } + + override fun onFinish() { + cvCountdown.setText(context.getString(R.string.timer_zero)) + playTone(ToneGenerator.TONE_SUP_CONFIRM, 500) + btnStop.visibility = View.GONE + btnRestart.visibility = View.GONE + btnDismiss.visibility = View.VISIBLE + } + }.start() + } + + private fun cancelTimer() { + timer?.cancel() + dismiss() + } + + private fun restartTimer() { + timer?.cancel() + startTimer() + } + + override fun dismiss() { + timer?.cancel() + toneGenerator?.release() + toneGenerator = null + super.dismiss() + } + + private fun playTone(tone: Int, durationMillis: Int? = null) { + if (durationMillis == null) { + toneGenerator?.startTone(tone) + } else { + toneGenerator?.startTone(tone, durationMillis) + } + } +} diff --git a/app/src/main/java/com/example/countdowntimer/MainActivity.kt b/app/src/main/java/com/example/countdowntimer/MainActivity.kt new file mode 100644 index 0000000..f007bfd --- /dev/null +++ b/app/src/main/java/com/example/countdowntimer/MainActivity.kt @@ -0,0 +1,18 @@ +package com.example.countdowntimer + +import android.os.Bundle +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + private val TIMER_DURATION_MS = 10000L + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + findViewById