Compare commits
38
Commits
be631985cb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c822484d | ||
|
|
d7ce314c22 | ||
|
|
da29e51993 | ||
|
|
7a91ecba18 | ||
|
|
97b01437b2 | ||
|
|
f87d23a922 | ||
|
|
b04f197ec2 | ||
|
|
634e0ae48a | ||
|
|
451569de17 | ||
|
|
075311d298 | ||
|
|
a3835e1404 | ||
|
|
bc259602aa | ||
|
|
3dd453e536 | ||
|
|
aca180cf20 | ||
|
|
b30427d862 | ||
|
|
eaaeb7fa35 | ||
|
|
6ba2f2cfdf | ||
|
|
51c7fa1464 | ||
|
|
df9d92f116 | ||
|
|
5b517f39d6 | ||
|
|
b20208fb40 | ||
|
|
9b733f2102 | ||
|
|
0738fc466e | ||
|
|
7491083e6a | ||
|
|
13786f6c8e | ||
|
|
92952a2cf2 | ||
|
|
6de400753c | ||
|
|
822d6fc1b1 | ||
|
|
b78b2109a7 | ||
|
|
2701bd22dd | ||
|
|
1273be03f2 | ||
|
|
cb5c9dc547 | ||
|
|
e00de3d340 | ||
|
|
b5442a6f7f | ||
|
|
41721bdc04 | ||
|
|
79afe50955 | ||
|
|
43bb282e74 | ||
|
|
d8ead34f0b |
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -5,6 +5,7 @@ gradle-app.setting
|
||||
# native lib outputs
|
||||
project-classes/
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.dex
|
||||
# Android Studio
|
||||
bin/
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
@@ -7,25 +7,29 @@
|
||||
- **Run Unit Tests**: `./gradlew test` (if applicable)
|
||||
|
||||
## Code Style Guidelines
|
||||
- **Language**: Kotlin 1.9+
|
||||
- **Language**: Kotlin 1.9.0
|
||||
- **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`)
|
||||
- Resource IDs: `camelCase` (e.g., `btnStart`, `cvCountdown`)
|
||||
- **Android Patterns**: Use `findViewById` for view binding in this project to maintain consistency with existing simple architecture.
|
||||
|
||||
## Architecture
|
||||
- **Pattern**: Simple Activity-Dialog architecture.
|
||||
- **State**: The countdown state is managed locally within `CountdownDialog` using `android.os.CountDownTimer`.
|
||||
- **Pattern**: Activity-Service architecture with `ForegroundService`.
|
||||
- **State**: The countdown state is managed in `CountdownService` using `android.os.CountDownTimer`, persisted to `SharedPreferences` for process death recovery.
|
||||
- **UI**: `MainActivity` binds to the service and acts as a thin UI projection, receiving state updates via `TimerStateListener` callback.
|
||||
- **Dependency Management**: Gradle with Groovy DSL.
|
||||
- **Key Constraints**:
|
||||
- The `CountdownDialog` is explicitly set to `setCancelable(false)` and `setCanceledOnTouchOutside(false)` to ensure the timer completes.
|
||||
- The notification is set to `setOngoing(true)` — cannot be swiped away.
|
||||
- Timer state survives process death via `SharedPreferences` persistence.
|
||||
|
||||
## Critical Files Reference
|
||||
- `app/src/main/java/com/example/countdowntimer/MainActivity.kt`: Entry point and trigger for the timer.
|
||||
- `app/src/main/java/com/example/countdowntimer/CountdownDialog.kt`: Implementation of the countdown logic and `ToneGenerator` trigger.
|
||||
- `app/src/main/res/layout/activity_main.xml`: Main screen layout.
|
||||
- `app/src/main/res/layout/dialog_countdown.xml`: Timer dialog layout.
|
||||
- `app/build.gradle`: Build configuration, SDK versions (minSdk 24, targetSdk 34), and dependencies.
|
||||
- `app/src/main/java/com/example/countdowntimer/MainActivity.kt`: Entry point, binds to service, implements `TimerStateListener`.
|
||||
- `app/src/main/java/com/example/countdowntimer/CountdownService.kt`: ForegroundService owning timer logic, `ToneGenerator`, `Vibrator`, notification, and state persistence.
|
||||
- `app/src/main/java/com/example/countdowntimer/TimerStateListener.kt`: Callback interface for service-to-activity state updates.
|
||||
- `app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt`: Custom view for the circular progress ring and countdown text.
|
||||
- `app/src/main/res/layout/activity_main.xml`: Main screen layout (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.
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
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.
|
||||
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
|
||||
- **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.
|
||||
- **Selectable Duration**: A main screen with a duration picker (chip group) and a start button.
|
||||
- **Modal Countdown**: A full-screen dialog with circular progress ring, stop/restart buttons, and a confirmation tone during the final 3 seconds.
|
||||
- **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.
|
||||
|
||||
## Getting Started
|
||||
@@ -25,19 +25,46 @@ To build the debug APK from the command line:
|
||||
```
|
||||
|
||||
### Running the App
|
||||
|
||||
#### Via Android Studio
|
||||
1. Open the project in Android Studio.
|
||||
2. Select your target device/emulator.
|
||||
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
|
||||
```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
|
||||
│ │ ├── MainActivity.kt # Main entry point; duration picker and timer trigger
|
||||
│ │ ├── CountdownDialog.kt # Countdown logic, tone, and vibration
|
||||
│ │ └── CountdownCanvasView.kt # Custom circular progress ring view
|
||||
│ ├── res/layout/ # UI layouts
|
||||
│ │ ├── activity_main.xml
|
||||
│ │ └── dialog_countdown.xml
|
||||
│ └── AndroidManifest.xml # Application manifest
|
||||
└── build.gradle # App-level build configuration
|
||||
```
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.countdowntimer"
|
||||
minSdk 24
|
||||
minSdk 34
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<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
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_timer"
|
||||
android:label="Countdown Timer"
|
||||
android:label="Bong Hit Timer"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.CountdownTimer">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".CountdownService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.example.countdowntimer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
@@ -14,12 +16,24 @@ class CountdownCanvasView @JvmOverloads constructor(
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private var textToDraw: String = ""
|
||||
private var progressFraction: Float = 1.0f
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#6750A4") // Using the same primary purple
|
||||
color = ContextCompat.getColor(context, R.color.md_theme_light_primary)
|
||||
textSize = 180f // Roughly equivalent to 72sp depending on density
|
||||
textAlign = Paint.Align.CENTER
|
||||
isFakeBoldText = false
|
||||
}
|
||||
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.md_color_ring_progress)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 16f
|
||||
}
|
||||
private val ringBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.md_color_ring_track)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 16f
|
||||
}
|
||||
private val rectF = RectF()
|
||||
|
||||
fun setText(text: String) {
|
||||
if (textToDraw != text) {
|
||||
@@ -28,15 +42,41 @@ class CountdownCanvasView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgress(fraction: Float) {
|
||||
progressFraction = fraction.coerceIn(0f, 1f)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setColor(color: Int) {
|
||||
if (ringPaint.color != color) {
|
||||
ringPaint.color = color
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
val inset = ringPaint.strokeWidth / 2f + 12f
|
||||
rectF.set(inset, inset, w - inset, h - inset)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (textToDraw.isEmpty()) return
|
||||
if (textToDraw.isEmpty() || rectF.width() <= 0f) return
|
||||
|
||||
val cx = rectF.centerX()
|
||||
val cy = rectF.centerY()
|
||||
|
||||
// Draw full background ring (always visible track)
|
||||
val bgRect = RectF(rectF.left, rectF.top, rectF.right, rectF.bottom)
|
||||
canvas.drawArc(bgRect, -90f, 360f, false, ringBgPaint)
|
||||
// Draw progress arc on top (remaining time, starts at 12 o'clock)
|
||||
canvas.drawArc(rectF, -90f, 360f * progressFraction, false, ringPaint)
|
||||
|
||||
// Draw text
|
||||
val fontMetrics = paint.fontMetrics
|
||||
val x = width / 2f
|
||||
val y = (height / 2f) - ((fontMetrics.descent + fontMetrics.ascent) / 2f)
|
||||
|
||||
canvas.drawText(textToDraw, x, y, paint)
|
||||
val textY = (cy - (fontMetrics.descent + fontMetrics.ascent) / 2f)
|
||||
canvas.drawText(textToDraw, cx, textY, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,18 +1,271 @@
|
||||
package com.example.countdowntimer
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.animation.AnticipateOvershootInterpolator
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val TIMER_DURATION_MS = 10000L
|
||||
class MainActivity : AppCompatActivity(), TimerStateListener {
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "timer_prefs"
|
||||
private const val KEY_SELECTED_DURATION = "selected_duration"
|
||||
private const val DEFAULT_DURATION = 600000L
|
||||
}
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var btnDurationLabel: Button
|
||||
private lateinit var hsvDurationOptions: View
|
||||
private lateinit var chipGroup: ChipGroup
|
||||
private lateinit var btnStart: View
|
||||
private lateinit var vibrator: Vibrator
|
||||
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) {
|
||||
300000L -> R.id.chip5m
|
||||
600000L -> R.id.chip10m
|
||||
900000L -> R.id.chip15m
|
||||
else -> R.id.chip10m
|
||||
}
|
||||
|
||||
private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
|
||||
R.id.chip5m -> 300000L
|
||||
R.id.chip10m -> 600000L
|
||||
R.id.chip15m -> 900000L
|
||||
R.id.chip30m -> 1800000L
|
||||
else -> DEFAULT_DURATION
|
||||
}
|
||||
|
||||
private fun updateDurationLabel() {
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
findViewById<Button>(R.id.btnStart).setOnClickListener {
|
||||
CountdownDialog(this, TIMER_DURATION_MS).show()
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION)
|
||||
|
||||
// Picker UI views
|
||||
btnDurationLabel = findViewById(R.id.btnDurationLabel)
|
||||
hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
|
||||
chipGroup = findViewById(R.id.chipGroup)
|
||||
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()
|
||||
|
||||
// Restore selected chip
|
||||
val chipId = durationToChipId(selectedDuration)
|
||||
chipGroup.check(chipId)
|
||||
|
||||
btnDurationLabel.setOnClickListener {
|
||||
if (hsvDurationOptions.visibility == View.GONE) {
|
||||
hsvDurationOptions.visibility = View.VISIBLE
|
||||
} else {
|
||||
hsvDurationOptions.visibility = View.GONE
|
||||
(currentFocus ?: btnDurationLabel).let { focused ->
|
||||
focused.clearFocus()
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(hsvDurationOptions.windowToken, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
val checkedId = if (checkedIds.size > 0) checkedIds[0].toInt() else -1
|
||||
if (checkedId != -1) {
|
||||
if (checkedId != lastSelectedChipId) {
|
||||
selectedDuration = chipIdToDuration(checkedId)
|
||||
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
|
||||
updateDurationLabel()
|
||||
lastSelectedChipId = checkedId
|
||||
}
|
||||
hsvDurationOptions.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.chip5m).setOnClickListener { onChipClicked(R.id.chip5m) }
|
||||
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>
|
||||
@@ -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,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6z"/>
|
||||
</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>
|
||||
@@ -14,55 +14,78 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
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">
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent=".85">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
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
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:id="@+id/timerIcon"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvInstructionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/instruction_title"
|
||||
android:textAlignment="center"
|
||||
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
|
||||
android:id="@+id/instructionsLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
android:src="@drawable/outline_fire_check_24"
|
||||
app:tint="@color/color_icon_tint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -72,16 +95,18 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
android:src="@drawable/outline_air_24"
|
||||
app:tint="@color/color_icon_tint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -91,16 +116,18 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
android:src="@drawable/ic_play_arrow"
|
||||
app:tint="@color/color_icon_tint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -110,17 +137,198 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnStart"
|
||||
style="@style/Widget.Material3.Button"
|
||||
<Button
|
||||
android:id="@+id/btnDurationLabel"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@drawable/btn_duration_selector"
|
||||
android:drawableEnd="@drawable/ic_keyboard_arrow_down"
|
||||
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
|
||||
android:id="@+id/hsvDurationOptions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/start_timer"
|
||||
app:cornerRadius="20dp"
|
||||
app:icon="@drawable/ic_play_arrow" />
|
||||
android:layout_marginTop="8dp"
|
||||
android:scrollbars="none"
|
||||
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
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleSelection="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip5m"
|
||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/chip10m"
|
||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="56dp"
|
||||
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
|
||||
android:id="@+id/chip15m"
|
||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/chip30m"
|
||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<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: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_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="center"
|
||||
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>
|
||||
|
||||
<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>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,53 +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="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintHeight_default="wrap"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/btnStop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:minWidth="120dp"
|
||||
android:minHeight="120dp" />
|
||||
|
||||
<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>
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="color_gradient_start">#2E1A47</color>
|
||||
<color name="color_gradient_end">#1B0B2E</color>
|
||||
<color name="color_gradient_start">#1B3A1B</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>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
<item name="colorPrimary">@color/md_theme_light_primary</item>
|
||||
<item name="colorSecondary">@color/md_theme_light_secondary</item>
|
||||
<item name="colorPrimary">@color/md_theme_dark_primary</item>
|
||||
<item name="colorSecondary">@color/md_theme_dark_secondary</item>
|
||||
<item name="android:statusBarColor">@color/color_gradient_start</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="color_gradient_start">#F3E5F5</color>
|
||||
<color name="color_gradient_end">#E1BEE7</color>
|
||||
<color name="color_icon_tint">#6750A4</color>
|
||||
<color name="color_gradient_start">#E8F5E9</color>
|
||||
<color name="color_gradient_end">#C8E6C9</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>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<resources>
|
||||
<string name="app_name">Countdown Timer</string>
|
||||
<string name="start_timer">Start Timer</string>
|
||||
<string name="countdown_text">!!! HOLD !!!</string>
|
||||
<string name="timer_zero">EXHALE!</string>
|
||||
<string name="countdown_text">0</string>
|
||||
<string name="timer_zero">AGAIN!</string>
|
||||
<string name="instruction_title">BONG HIT TIMER!</string>
|
||||
<string name="instruction_volume">Ensure your bong is loaded</string>
|
||||
<string name="instruction_position">Find a comfortable position</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_volume">When the timer elapses</string>
|
||||
<string name="instruction_position">Take your hit</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="settings">Settings</string>
|
||||
<string name="vibration">Vibration</string>
|
||||
<string name="sounds">Sounds</string>
|
||||
<string name="stop_timer">Stop Timer</string>
|
||||
<string name="restart_timer">Restart Timer</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<resources>
|
||||
<style name="Theme.CountdownTimer" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<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="android:statusBarColor">@color/color_gradient_start</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
|
||||
<color name="md_theme_light_primary">#6750A4</color>
|
||||
<color name="md_theme_light_secondary">#625B71</color>
|
||||
<color name="md_theme_light_surface">#FEF7FF</color>
|
||||
<color name="md_theme_light_primary">#2E7D32</color>
|
||||
<color name="md_theme_light_primaryContainer">#81C784</color>
|
||||
<color name="md_theme_light_secondary">#4C6842</color>
|
||||
<color name="md_theme_light_surface">#F1F8E9</color>
|
||||
</resources>
|
||||
|
||||
@@ -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 |
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user