Compare commits
6
Commits
b04f197ec2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c822484d | ||
|
|
d7ce314c22 | ||
|
|
da29e51993 | ||
|
|
7a91ecba18 | ||
|
|
97b01437b2 | ||
|
|
f87d23a922 |
+26
-24
@@ -1,31 +1,33 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/java:dev-17-jdk-trixie
|
FROM ghcr.io/alvr/alpine-android:android-34-jdk21
|
||||||
|
|
||||||
# Install Android SDK command-line tools
|
# Install Android SDK command-line tools
|
||||||
# ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
|
# ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
|
||||||
ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
|
# ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
|
||||||
ARG ANDROID_SDK_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
|
# ARG ANDROID_SDK_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
|
||||||
|
|
||||||
RUN mkdir -p /usr/local/android-sdk/cmdline-tools && \
|
# RUN apk update && apk add curl zip git openjdk21-jdk cmake gcc
|
||||||
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
|
# RUN mkdir -p /usr/local/android-sdk/cmdline-tools && \
|
||||||
ENV ANDROID_HOME=/usr/local/android-sdk
|
# cd /tmp && \
|
||||||
ENV ANDROID_COMMAND_LINE_TOOL=/usr/local/android-sdk/cmdline-tools/latest/bin
|
# curl -fsSL "$ANDROID_SDK_URL" -o cmdline-tools.zip && \
|
||||||
ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
# 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
|
||||||
|
|
||||||
# Accept licenses and install required SDK packages
|
# # Set Android SDK paths and environment
|
||||||
RUN mkdir -p $ANDROID_HOME/.android && \
|
# ENV ANDROID_HOME=/usr/local/android-sdk
|
||||||
touch $ANDROID_HOME/.android/repositories.cfg && \
|
# ENV ANDROID_COMMAND_LINE_TOOL=/usr/local/android-sdk/cmdline-tools/latest/bin
|
||||||
yes | ${ANDROID_COMMAND_LINE_TOOL}/sdkmanager --licenses 2>/dev/null || true && \
|
# ENV PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
||||||
${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
|
# # Accept licenses and install required SDK packages
|
||||||
RUN chmod +x /workspaces/*/gradlew 2>/dev/null || true
|
# 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
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"redhat.vscode-xml",
|
"redhat.vscode-xml",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"felipecaslazana.vscode-android-logcat",
|
"felipecaslazana.vscode-android-logcat",
|
||||||
"ms-azuretools.vscode-docker"
|
"ms-azuretools.vscode-docker",
|
||||||
|
"fwcd.kotlin",
|
||||||
|
"vscjava.vscode-java-pack",
|
||||||
|
"Anthropic.claude-code"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,16 +17,19 @@
|
|||||||
- **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/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/java/com/example/countdowntimer/CountdownCanvasView.kt`: Custom view for the circular progress ring and countdown text.
|
||||||
- `app/src/main/res/layout/activity_main.xml`: Main screen layout.
|
- `app/src/main/res/layout/activity_main.xml`: Main screen layout (contains both picker and countdown UI).
|
||||||
- `app/src/main/res/layout/dialog_countdown.xml`: Timer dialog layout.
|
- `app/src/main/AndroidManifest.xml`: Declares service and foreground service permissions.
|
||||||
- `app/build.gradle`: Build configuration, SDK versions (minSdk 24, targetSdk 34), and dependencies.
|
- `app/build.gradle`: Build configuration, SDK versions (minSdk 34, targetSdk 34), and dependencies.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?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"
|
||||||
@@ -16,5 +18,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".CountdownService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ class CountdownCanvasView @JvmOverloads constructor(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setColor(color: Int) {
|
||||||
|
if (ringPaint.color != color) {
|
||||||
|
ringPaint.color = color
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
super.onSizeChanged(w, h, oldw, oldh)
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
val inset = ringPaint.strokeWidth / 2f + 12f
|
val inset = ringPaint.strokeWidth / 2f + 12f
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package com.example.countdowntimer
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.media.ToneGenerator
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.CountDownTimer
|
|
||||||
import android.os.Vibrator
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Button
|
|
||||||
|
|
||||||
class CountdownDialog(
|
|
||||||
private val context: Context,
|
|
||||||
private val durationMillis: Long,
|
|
||||||
private val prefs: SharedPreferences
|
|
||||||
) : Dialog(context) {
|
|
||||||
private lateinit var cvCountdown: CountdownCanvasView
|
|
||||||
private lateinit var btnStop: View
|
|
||||||
private lateinit var btnRestart: View
|
|
||||||
private lateinit var btnDismiss: Button
|
|
||||||
private var timer: CountDownTimer? = null
|
|
||||||
private var toneGenerator: ToneGenerator? = null
|
|
||||||
private var vibrator: Vibrator? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.dialog_countdown)
|
|
||||||
|
|
||||||
setCanceledOnTouchOutside(false)
|
|
||||||
setCancelable(false)
|
|
||||||
|
|
||||||
window?.let { win ->
|
|
||||||
win.setBackgroundDrawableResource(android.R.color.transparent)
|
|
||||||
val lp = win.attributes
|
|
||||||
lp.width = WindowManager.LayoutParams.MATCH_PARENT
|
|
||||||
lp.height = WindowManager.LayoutParams.MATCH_PARENT
|
|
||||||
win.attributes = lp
|
|
||||||
}
|
|
||||||
|
|
||||||
cvCountdown = findViewById(R.id.cvCountdown)
|
|
||||||
btnStop = findViewById(R.id.btnStop)
|
|
||||||
btnRestart = findViewById(R.id.btnRestart)
|
|
||||||
btnDismiss = findViewById(R.id.btnDismiss)
|
|
||||||
toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
|
|
||||||
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
||||||
|
|
||||||
btnStop.setOnClickListener { cancelTimer() }
|
|
||||||
btnRestart.setOnClickListener { restartTimer() }
|
|
||||||
btnDismiss.setOnClickListener { dismiss() }
|
|
||||||
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTimer() {
|
|
||||||
cvCountdown.setProgress(1f)
|
|
||||||
timer = object : CountDownTimer(durationMillis, 1000) {
|
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
|
||||||
val progress = millisUntilFinished.toFloat() / durationMillis
|
|
||||||
cvCountdown.setProgress(progress)
|
|
||||||
|
|
||||||
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
|
|
||||||
cvCountdown.setText(secondsLeft.toString())
|
|
||||||
|
|
||||||
if (secondsLeft >= 1 && secondsLeft <= 3) {
|
|
||||||
playTone(ToneGenerator.TONE_PROP_BEEP)
|
|
||||||
if (prefs.getBoolean("vibration", true)) {
|
|
||||||
vibrator?.vibrate(50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
cvCountdown.setText(context.getString(R.string.timer_zero))
|
|
||||||
cvCountdown.setProgress(0f)
|
|
||||||
playTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
|
|
||||||
if (prefs.getBoolean("vibration", true)) {
|
|
||||||
vibrator?.vibrate(200)
|
|
||||||
}
|
|
||||||
btnStop.visibility = View.GONE
|
|
||||||
btnRestart.visibility = View.GONE
|
|
||||||
btnDismiss.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelTimer() {
|
|
||||||
timer?.cancel()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restartTimer() {
|
|
||||||
timer?.cancel()
|
|
||||||
startTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dismiss() {
|
|
||||||
timer?.cancel()
|
|
||||||
toneGenerator?.release()
|
|
||||||
toneGenerator = null
|
|
||||||
super.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playTone(tone: Int, durationMillis: Int? = null) {
|
|
||||||
if (durationMillis == null) {
|
|
||||||
toneGenerator?.startTone(tone)
|
|
||||||
} else {
|
|
||||||
toneGenerator?.startTone(tone, durationMillis)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package com.example.countdowntimer
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.ToneGenerator
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
|
||||||
|
class CountdownService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_START = "com.example.countdowntimer.START"
|
||||||
|
const val ACTION_STOP = "com.example.countdowntimer.STOP"
|
||||||
|
const val ACTION_RESTART = "com.example.countdowntimer.RESTART"
|
||||||
|
const val EXTRA_DURATION = "extra_duration"
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
private const val CHANNEL_ID = "countdown_channel"
|
||||||
|
private const val CHANNEL_NAME = "Countdown Timer"
|
||||||
|
private const val PREFS_NAME = "timer_prefs"
|
||||||
|
private const val KEY_REMAINING_MILLIS = "remaining_millis"
|
||||||
|
private const val KEY_TOTAL_DURATION = "total_duration"
|
||||||
|
private const val KEY_TIMER_RUNNING = "timer_running"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
private var toneGenerator: ToneGenerator? = null
|
||||||
|
private var vibrator: Vibrator? = null
|
||||||
|
private var prefs: SharedPreferences? = null
|
||||||
|
private var stateListener: TimerStateListener? = null
|
||||||
|
private var durationMillis: Long = 0
|
||||||
|
private var notificationManager: NotificationManager? = null
|
||||||
|
private var isTimerRunning: Boolean = false
|
||||||
|
|
||||||
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
fun getService(): CountdownService = this@CountdownService
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStateListener(listener: TimerStateListener?) {
|
||||||
|
stateListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRunning(): Boolean = isTimerRunning
|
||||||
|
|
||||||
|
// ---- Service lifecycle ----
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
|
||||||
|
|
||||||
|
val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
vibrator = vibratorManager.defaultVibrator
|
||||||
|
|
||||||
|
createNotificationChannel()
|
||||||
|
|
||||||
|
// Restore timer state if it was running before process death
|
||||||
|
if (prefs?.getBoolean(KEY_TIMER_RUNNING, false) == true) {
|
||||||
|
val remaining = prefs?.getLong(KEY_REMAINING_MILLIS, 0) ?: 0
|
||||||
|
durationMillis = prefs?.getLong(KEY_TOTAL_DURATION, 0) ?: 0
|
||||||
|
if (remaining > 0 && durationMillis > 0) {
|
||||||
|
resumeTimer(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val action = intent?.action
|
||||||
|
when (action) {
|
||||||
|
ACTION_START -> {
|
||||||
|
val duration = intent.getLongExtra(EXTRA_DURATION, 600000L)
|
||||||
|
startTimer(duration)
|
||||||
|
}
|
||||||
|
ACTION_STOP -> stopTimer()
|
||||||
|
ACTION_RESTART -> restartTimer()
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder = binder
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = null
|
||||||
|
toneGenerator?.release()
|
||||||
|
toneGenerator = null
|
||||||
|
notificationManager?.cancel(NOTIFICATION_ID)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Timer logic ----
|
||||||
|
|
||||||
|
private fun startTimer(duration: Long) {
|
||||||
|
durationMillis = duration
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = createCountDownTimer(duration).start()
|
||||||
|
isTimerRunning = true
|
||||||
|
|
||||||
|
persistState(duration, duration)
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification(duration, duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resumeTimer(remaining: Long) {
|
||||||
|
isTimerRunning = true
|
||||||
|
countDownTimer = createCountDownTimer(remaining).start()
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification(remaining, durationMillis))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopTimer() {
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
countDownTimer = null
|
||||||
|
isTimerRunning = false
|
||||||
|
|
||||||
|
prefs?.edit()?.apply {
|
||||||
|
putBoolean(KEY_TIMER_RUNNING, false)
|
||||||
|
remove(KEY_REMAINING_MILLIS)
|
||||||
|
remove(KEY_TOTAL_DURATION)
|
||||||
|
}?.apply()
|
||||||
|
|
||||||
|
notificationManager?.cancel(NOTIFICATION_ID)
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
stateListener?.onTimerStopped()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restartTimer() {
|
||||||
|
countDownTimer?.cancel()
|
||||||
|
startTimer(durationMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistState(remaining: Long, total: Long) {
|
||||||
|
prefs?.edit()?.apply {
|
||||||
|
putLong(KEY_REMAINING_MILLIS, remaining)
|
||||||
|
putLong(KEY_TOTAL_DURATION, total)
|
||||||
|
putBoolean(KEY_TIMER_RUNNING, true)
|
||||||
|
}?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCountDownTimer(remaining: Long): CountDownTimer {
|
||||||
|
return object : CountDownTimer(remaining, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val progress = millisUntilFinished.toFloat() / durationMillis
|
||||||
|
|
||||||
|
// Persist remaining time
|
||||||
|
persistState(millisUntilFinished, durationMillis)
|
||||||
|
|
||||||
|
// Color interpolation: green → yellow at 50% → red at end
|
||||||
|
val green = android.graphics.Color.parseColor("#2E7D32")
|
||||||
|
val yellow = android.graphics.Color.parseColor("#FFC107")
|
||||||
|
val red = android.graphics.Color.parseColor("#F44336")
|
||||||
|
val ringColor = when {
|
||||||
|
progress >= 0.5f -> {
|
||||||
|
val t = (progress - 0.5f) * 2f
|
||||||
|
ColorUtils.blendARGB(green, yellow, t)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val t = progress * 2f
|
||||||
|
ColorUtils.blendARGB(yellow, red, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
|
||||||
|
|
||||||
|
// Warning beeps and vibration in last 3 seconds
|
||||||
|
if (secondsLeft >= 1 && secondsLeft <= 3) {
|
||||||
|
toneGenerator?.startTone(ToneGenerator.TONE_PROP_BEEP)
|
||||||
|
vibrator?.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update notification
|
||||||
|
updateNotification(millisUntilFinished, durationMillis)
|
||||||
|
|
||||||
|
// Push state to activity
|
||||||
|
stateListener?.onTimerTick(millisUntilFinished, durationMillis, secondsLeft, ringColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
isTimerRunning = false
|
||||||
|
prefs?.edit()?.putBoolean(KEY_TIMER_RUNNING, false)?.apply()
|
||||||
|
|
||||||
|
toneGenerator?.startTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
|
||||||
|
vibrator?.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
|
||||||
|
updateNotification(0, durationMillis)
|
||||||
|
stateListener?.onTimerFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Notification ----
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
description = "Countdown timer progress"
|
||||||
|
lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
|
||||||
|
}
|
||||||
|
notificationManager?.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(remainingMillis: Long, totalMillis: Long): android.app.Notification {
|
||||||
|
val secondsRemaining = (remainingMillis / 1000).toInt()
|
||||||
|
val minutes = secondsRemaining / 60
|
||||||
|
val seconds = secondsRemaining % 60
|
||||||
|
val timeText = String.format("%02d:%02d", minutes, seconds)
|
||||||
|
val progressPercent = if (totalMillis > 0) {
|
||||||
|
((1 - remainingMillis.toFloat() / totalMillis) * 100).toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val stopPendingIntent = PendingIntent.getService(
|
||||||
|
this, 0,
|
||||||
|
Intent(this, CountdownService::class.java).apply { action = ACTION_STOP },
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val restartPendingIntent = PendingIntent.getService(
|
||||||
|
this, 1,
|
||||||
|
Intent(this, CountdownService::class.java).apply { action = ACTION_RESTART },
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val activityPendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 2,
|
||||||
|
Intent(this, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("Bong Hit Timer")
|
||||||
|
.setContentText(timeText)
|
||||||
|
.setSmallIcon(R.drawable.ic_timer)
|
||||||
|
.setProgress(100, progressPercent, false)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(R.drawable.ic_stop_square, "Stop", stopPendingIntent)
|
||||||
|
.addAction(R.drawable.ic_refresh_square, "Restart", restartPendingIntent)
|
||||||
|
.setContentIntent(activityPendingIntent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification(remainingMillis: Long, totalMillis: Long) {
|
||||||
|
notificationManager?.notify(NOTIFICATION_ID, buildNotification(remainingMillis, totalMillis))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,66 @@
|
|||||||
package com.example.countdowntimer
|
package com.example.countdowntimer
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.view.animation.AnticipateOvershootInterpolator
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity(), TimerStateListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "timer_prefs"
|
private const val PREFS_NAME = "timer_prefs"
|
||||||
private const val KEY_SELECTED_DURATION = "selected_duration"
|
private const val KEY_SELECTED_DURATION = "selected_duration"
|
||||||
private const val DEFAULT_DURATION = 10000L
|
private const val DEFAULT_DURATION = 600000L
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
private lateinit var btnDurationLabel: Button
|
private lateinit var btnDurationLabel: Button
|
||||||
private lateinit var hsvDurationOptions: View
|
private lateinit var hsvDurationOptions: View
|
||||||
private lateinit var chipGroup: ChipGroup
|
private lateinit var chipGroup: ChipGroup
|
||||||
|
private lateinit var btnStart: View
|
||||||
|
private lateinit var vibrator: Vibrator
|
||||||
private var selectedDuration: Long = DEFAULT_DURATION
|
private var selectedDuration: Long = DEFAULT_DURATION
|
||||||
|
private var lastSelectedChipId: Int = -1
|
||||||
|
private var pulseAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
// Service binding
|
||||||
|
private var service: CountdownService? = null
|
||||||
|
private var serviceBound = false
|
||||||
|
private lateinit var serviceConnection: ServiceConnection
|
||||||
|
|
||||||
|
// Countdown UI views
|
||||||
|
private lateinit var cvCountdown: CountdownCanvasView
|
||||||
|
private lateinit var btnStop: View
|
||||||
|
private lateinit var btnRestart: View
|
||||||
|
private lateinit var btnDismiss: Button
|
||||||
|
private lateinit var countdownLayout: View
|
||||||
|
private lateinit var pickerLayout: View
|
||||||
|
|
||||||
private fun durationToChipId(duration: Long): Int = when (duration) {
|
private fun durationToChipId(duration: Long): Int = when (duration) {
|
||||||
5000L -> R.id.chip5s
|
300000L -> R.id.chip5m
|
||||||
15000L -> R.id.chip15s
|
600000L -> R.id.chip10m
|
||||||
30000L -> R.id.chip30s
|
900000L -> R.id.chip15m
|
||||||
else -> R.id.chip10s
|
else -> R.id.chip10m
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
|
private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
|
||||||
R.id.chip5s -> 5000L
|
R.id.chip5m -> 300000L
|
||||||
R.id.chip10s -> 10000L
|
R.id.chip10m -> 600000L
|
||||||
R.id.chip15s -> 15000L
|
R.id.chip15m -> 900000L
|
||||||
R.id.chip30s -> 30000L
|
R.id.chip30m -> 1800000L
|
||||||
else -> DEFAULT_DURATION
|
else -> DEFAULT_DURATION
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +68,98 @@ class MainActivity : AppCompatActivity() {
|
|||||||
btnDurationLabel.text = "${selectedDuration / 1000}s"
|
btnDurationLabel.text = "${selectedDuration / 1000}s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onChipClicked(chipId: Int) {
|
||||||
|
if (chipId == lastSelectedChipId) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createOneShot(15, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
val originalText = btnDurationLabel.text
|
||||||
|
btnDurationLabel.text = "${selectedDuration / 1000}s!"
|
||||||
|
btnDurationLabel.postDelayed({ btnDurationLabel.text = originalText }, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPulseAnimation() {
|
||||||
|
pulseAnimator = ValueAnimator.ofFloat(1f, 0.78f, 1f).apply {
|
||||||
|
duration = 1500
|
||||||
|
repeatMode = ValueAnimator.REVERSE
|
||||||
|
repeatCount = ValueAnimator.INFINITE
|
||||||
|
interpolator = AnticipateOvershootInterpolator(1f)
|
||||||
|
addUpdateListener { animation ->
|
||||||
|
val fraction = animation.animatedValue as Float
|
||||||
|
btnStart.scaleX = fraction
|
||||||
|
btnStart.scaleY = fraction
|
||||||
|
btnStart.alpha = fraction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pulseAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPulseAnimation() {
|
||||||
|
pulseAnimator?.cancel()
|
||||||
|
pulseAnimator = null
|
||||||
|
btnStart.scaleX = 1f
|
||||||
|
btnStart.scaleY = 1f
|
||||||
|
btnStart.alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCountdownUI() {
|
||||||
|
pickerLayout.visibility = View.GONE
|
||||||
|
countdownLayout.visibility = View.VISIBLE
|
||||||
|
stopPulseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideCountdownUI() {
|
||||||
|
countdownLayout.visibility = View.GONE
|
||||||
|
pickerLayout.visibility = View.VISIBLE
|
||||||
|
btnDismiss.visibility = View.GONE
|
||||||
|
btnStop.visibility = View.VISIBLE
|
||||||
|
btnRestart.visibility = View.VISIBLE
|
||||||
|
startPulseAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- ServiceConnection ----
|
||||||
|
|
||||||
|
private fun initServiceConnection() {
|
||||||
|
serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||||
|
val localBinder = binder as CountdownService.LocalBinder
|
||||||
|
service = localBinder.getService()
|
||||||
|
serviceBound = true
|
||||||
|
service?.setStateListener(this@MainActivity)
|
||||||
|
if (service?.isRunning() == true) {
|
||||||
|
showCountdownUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
serviceBound = false
|
||||||
|
service = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- TimerStateListener callbacks ----
|
||||||
|
|
||||||
|
override fun onTimerTick(remainingMillis: Long, totalDuration: Long, secondsLeft: Int, ringColor: Int) {
|
||||||
|
val progress = remainingMillis.toFloat() / totalDuration
|
||||||
|
cvCountdown.setProgress(progress)
|
||||||
|
cvCountdown.setColor(ringColor)
|
||||||
|
cvCountdown.setText(secondsLeft.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimerFinished() {
|
||||||
|
cvCountdown.setText(getString(R.string.timer_zero))
|
||||||
|
cvCountdown.setProgress(0f)
|
||||||
|
btnStop.visibility = View.GONE
|
||||||
|
btnRestart.visibility = View.GONE
|
||||||
|
btnDismiss.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimerStopped() {
|
||||||
|
hideCountdownUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Activity lifecycle ----
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
@@ -49,9 +167,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION)
|
selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION)
|
||||||
|
|
||||||
|
// Picker UI views
|
||||||
btnDurationLabel = findViewById(R.id.btnDurationLabel)
|
btnDurationLabel = findViewById(R.id.btnDurationLabel)
|
||||||
hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
|
hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
|
||||||
chipGroup = findViewById(R.id.chipGroup)
|
chipGroup = findViewById(R.id.chipGroup)
|
||||||
|
btnStart = findViewById(R.id.btnStart)
|
||||||
|
val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
vibrator = vibratorManager.defaultVibrator
|
||||||
|
|
||||||
|
// Countdown UI views
|
||||||
|
cvCountdown = findViewById(R.id.cvCountdown)
|
||||||
|
btnStop = findViewById(R.id.btnStop)
|
||||||
|
btnRestart = findViewById(R.id.btnRestart)
|
||||||
|
btnDismiss = findViewById(R.id.btnDismiss)
|
||||||
|
countdownLayout = findViewById(R.id.countdownLayout)
|
||||||
|
pickerLayout = findViewById(R.id.pickerLayout)
|
||||||
|
|
||||||
updateDurationLabel()
|
updateDurationLabel()
|
||||||
|
|
||||||
@@ -72,17 +202,70 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chipGroup.setOnCheckedChangeListener { _, checkedId ->
|
chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||||
|
val checkedId = if (checkedIds.size > 0) checkedIds[0].toInt() else -1
|
||||||
if (checkedId != -1) {
|
if (checkedId != -1) {
|
||||||
selectedDuration = chipIdToDuration(checkedId)
|
if (checkedId != lastSelectedChipId) {
|
||||||
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
|
selectedDuration = chipIdToDuration(checkedId)
|
||||||
updateDurationLabel()
|
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
|
||||||
|
updateDurationLabel()
|
||||||
|
lastSelectedChipId = checkedId
|
||||||
|
}
|
||||||
hsvDurationOptions.visibility = View.GONE
|
hsvDurationOptions.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<View>(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()
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:state_checked="true" android:color="#81C784" />
|
<item android:state_checked="true" android:color="#2E7D32" />
|
||||||
<item android:color="#E8E8E8" />
|
<item android:color="#233024" />
|
||||||
</selector>
|
</selector>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_checked="true" android:color="#C8E6C9" />
|
||||||
|
<item android:color="#2E7D32" />
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_checked="true" android:color="#FFFFFF" />
|
||||||
|
<item android:color="#C8E6C9" />
|
||||||
|
</selector>
|
||||||
@@ -29,210 +29,304 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="24dp">
|
android:padding="24dp">
|
||||||
|
|
||||||
<ImageView
|
<!-- Picker UI -->
|
||||||
android:id="@+id/timerIcon"
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="80dp"
|
android:id="@+id/pickerLayout"
|
||||||
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="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:visibility="visible"
|
||||||
android:gravity="center"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:text="@string/instruction_title"
|
|
||||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<LinearLayout
|
<ImageView
|
||||||
android:id="@+id/instructionsLayout"
|
android:id="@+id/timerIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="80dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="80dp"
|
||||||
android:layout_marginTop="24dp"
|
android:src="@drawable/ic_timer"
|
||||||
android:orientation="vertical"
|
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_toBottomOf="@+id/tvInstructionTitle">
|
app:tint="@color/color_icon_tint" />
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
|
android:id="@+id/tvInstructionTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:layout_marginTop="16dp"
|
||||||
android:orientation="horizontal"
|
android:gravity="center"
|
||||||
android:padding="8dp">
|
android:text="@string/instruction_title"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
|
||||||
<ImageView
|
android:textStyle="bold"
|
||||||
android:layout_width="24dp"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:layout_height="24dp"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:src="@drawable/outline_fire_check_24"
|
app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
|
||||||
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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/instructionsLayout"
|
||||||
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/outline_air_24"
|
|
||||||
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="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_play_arrow"
|
|
||||||
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>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<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="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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:singleSelection="true">
|
android:layout_marginTop="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvInstructionTitle">
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<LinearLayout
|
||||||
android:id="@+id/chip5s"
|
android:layout_width="match_parent"
|
||||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
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/outline_fire_check_24"
|
||||||
|
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="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/outline_air_24"
|
||||||
|
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="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_play_arrow"
|
||||||
|
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>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<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="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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="56dp"
|
app:singleSelection="true">
|
||||||
android:text="5s"
|
|
||||||
app:chipBackgroundColor="@color/chip_duration_bg" />
|
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/chip10s"
|
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:checked="true"
|
android:minWidth="56dp"
|
||||||
android:minWidth="56dp"
|
android:text="5m"
|
||||||
android:text="10s"
|
android:textColor="@color/chip_duration_text"
|
||||||
app:chipBackgroundColor="@color/chip_duration_bg" />
|
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/chip10m"
|
||||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="56dp"
|
android:minWidth="56dp"
|
||||||
android:text="15s"
|
android:text="10m"
|
||||||
app:chipBackgroundColor="@color/chip_duration_bg" />
|
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/chip15m"
|
||||||
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="56dp"
|
android:minWidth="56dp"
|
||||||
android:text="30s"
|
android:text="15m"
|
||||||
app:chipBackgroundColor="@color/chip_duration_bg" />
|
android:textColor="@color/chip_duration_text"
|
||||||
</com.google.android.material.chip.ChipGroup>
|
app:chipBackgroundColor="@color/chip_duration_bg"
|
||||||
</HorizontalScrollView>
|
app:chipStrokeColor="@color/chip_duration_stroke"
|
||||||
|
app:chipStrokeWidth="2dp" />
|
||||||
|
|
||||||
<View
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/spacerBelowChips"
|
android:id="@+id/chip30m"
|
||||||
android:layout_width="1dp"
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
android:layout_height="5dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/hsvDurationOptions" />
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<FrameLayout
|
<com.example.countdowntimer.CountdownCanvasView
|
||||||
android:id="@+id/btnStart"
|
android:id="@+id/cvCountdown"
|
||||||
android:layout_width="100dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="16dp"
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
android:background="@drawable/btn_start_selector"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:clickable="true"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:focusable="true"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintWidth_percent="0.9" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/spacerBelowChips">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/buttonRow"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="Start"
|
android:layout_marginTop="24dp"
|
||||||
android:padding="20dp"
|
android:layout_marginBottom="24dp"
|
||||||
android:src="@drawable/ic_play_arrow"
|
android:gravity="center"
|
||||||
android:tint="@android:color/white" />
|
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>
|
<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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="#80000000">
|
|
||||||
|
|
||||||
<com.example.countdowntimer.CountdownCanvasView
|
|
||||||
android:id="@+id/cvCountdown"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/buttonRow"
|
|
||||||
app:layout_constraintDimensionRatio="1:1"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/buttonRow"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="48dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/btnStop"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:background="@drawable/btn_stop_selector"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/stop_timer"
|
|
||||||
android:src="@drawable/ic_stop_square" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/btnRestart"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:background="@drawable/btn_restart_selector"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/restart_timer"
|
|
||||||
android:src="@drawable/ic_refresh_square" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnDismiss"
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:background="@drawable/btn_duration_selector"
|
|
||||||
android:gravity="center"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:text="Dismiss"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Countdown Timer</string>
|
<string name="app_name">Countdown Timer</string>
|
||||||
<string name="start_timer">Start Timer</string>
|
<string name="start_timer">Start Timer</string>
|
||||||
<string name="countdown_text">!!! HOLD !!!</string>
|
<string name="countdown_text">0</string>
|
||||||
<string name="timer_zero">EXHALE!</string>
|
<string name="timer_zero">AGAIN!</string>
|
||||||
<string name="instruction_title">BONG HIT TIMER!</string>
|
<string name="instruction_title">BONG HIT TIMER!</string>
|
||||||
<string name="instruction_volume">Light your bowl</string>
|
<string name="instruction_volume">When the timer elapses</string>
|
||||||
<string name="instruction_position">Take your hit</string>
|
<string name="instruction_position">Take your hit</string>
|
||||||
<string name="instruction_distractions">Press Start to time how long to hold it!</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>
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
# Goal: Redesign all buttons into bold, square, visually striking interactive elements
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Replace every basic/rectangular button in the app with larger, square-aspect-ratio buttons that have eye-catching visual design. The goal is to transform the UI from a generic Material 3 look into a distinctive, personality-rich interface that matches the playful "BONG HIT TIMER" brand.
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
The app uses 4 visible buttons across 2 screens:
|
|
||||||
|
|
||||||
**MainActivity (home screen):**
|
|
||||||
- `btnDurationLabel` — outlined button showing selected duration (e.g. "10s") with a dropdown arrow. Uses `Widget.Material3.Button.OutlinedButton` with 20dp corners.
|
|
||||||
- `btnStart` — filled button labeled "Start Timer" with a play icon. Uses `Widget.Material3.Button` with 20dp corners.
|
|
||||||
|
|
||||||
**CountdownDialog (timer screen):**
|
|
||||||
- `btnStop` — plain Android `Button` labeled "Stop" (default styling, small)
|
|
||||||
- `btnRestart` — plain Android `Button` labeled "Restart" (default styling, small)
|
|
||||||
- `btnDismiss` — plain Android `Button` labeled "Dismiss" (currently gone, shows after timer completes)
|
|
||||||
|
|
||||||
## Design Requirements
|
|
||||||
|
|
||||||
### 1. Square Aspect Ratio
|
|
||||||
- Change all buttons from `wrap_content` rectangles to a fixed square size (e.g., `120dp` x `120dp` or `100dp` x `100dp`)
|
|
||||||
- Use `app:layout_constraintDimensionRatio="1:1"` where dynamic sizing is needed
|
|
||||||
- Buttons should feel substantial and tappable, not compact
|
|
||||||
|
|
||||||
### 2. Visual Interest — Each button should have its own personality
|
|
||||||
Create custom drawable backgrounds for each button type. Design ideas (pick the best approach):
|
|
||||||
|
|
||||||
- **Circular/spherical buttons** — Buttons that look like physical 3D balls or pucks. Use gradient fills with highlights/shadows to create a convex "pressable" look.
|
|
||||||
- **Neon/glow effect** — Since the app is a bong hit timer, a subtle neon glow that pulses or intensifies during countdown would be thematically appropriate.
|
|
||||||
- **Retro/gaming arcade style** — Bold colors, thick borders, pressed/active states that simulate physical button depression.
|
|
||||||
- **Gradient + icon centering** — For the timer screen, center the stop/restart icons prominently in the square buttons with bold gradient backgrounds.
|
|
||||||
|
|
||||||
### 3. Interactive States
|
|
||||||
Every button must have rich feedback:
|
|
||||||
- **Pressed/active state** — noticeable visual change (shrink, darker shade, or inner glow)
|
|
||||||
- **Default state** — visually engaging with gradients, shadows, or subtle animations
|
|
||||||
- **Disabled state** — reduced opacity but still recognizable
|
|
||||||
|
|
||||||
### 4. Icon Integration
|
|
||||||
Square buttons are perfect for icon-first design:
|
|
||||||
- **Stop button** — large centered pause/stop icon
|
|
||||||
- **Restart button** — large centered refresh/replay icon
|
|
||||||
- **Start button** — center the play icon, consider removing the text label since the icon is universal
|
|
||||||
- **Duration label** — consider a pill-shaped or circular chip that pulses when tapped
|
|
||||||
|
|
||||||
### 5. Color Palette
|
|
||||||
Stay within the existing green theme (`#2E7D32` primary, `#4C6842` secondary, `#E8F5E9`/`#C8E6C9` gradient) but push it further:
|
|
||||||
- Use the primary green for the start/positive actions
|
|
||||||
- A red/coral accent for stop
|
|
||||||
- A neutral or blue for restart/duration selection
|
|
||||||
- Consider gradient transitions within each button (e.g., green to darker green top-to-bottom for a 3D effect)
|
|
||||||
|
|
||||||
### 6. Shadow & Elevation
|
|
||||||
- Add drop shadows to all buttons for depth (use `app:strokeWidth="0dp"` and custom `rippleColor` for Material buttons)
|
|
||||||
- Create a `btn_square_default.xml` and `btn_square_pressed.xml` in `res/drawable/` for each button type
|
|
||||||
- The pressed state should visually simulate "pressing in" (reduced shadow, lighter/darker fill)
|
|
||||||
|
|
||||||
## Technical Implementation
|
|
||||||
|
|
||||||
### New drawable files to create (in `res/drawable/`):
|
|
||||||
- `btn_start_square.xml` — gradient + shadow drawable for start button
|
|
||||||
- `btn_start_square_pressed.xml` — pressed state variant
|
|
||||||
- `btn_stop_square.xml` — gradient + shadow drawable for stop button
|
|
||||||
- `btn_stop_square_pressed.xml` — pressed state variant
|
|
||||||
- `btn_restart_square.xml` — gradient + shadow drawable for restart button
|
|
||||||
- `btn_restart_square_pressed.xml` — pressed state variant
|
|
||||||
- `btn_duration_chip.xml` — drawable for duration selector
|
|
||||||
|
|
||||||
### Layout changes needed:
|
|
||||||
- `activity_main.xml`:
|
|
||||||
- `btnDurationLabel`: change to square dimensions, wrap text/icon in center, apply new drawable
|
|
||||||
- `btnStart`: change to square, center play icon, apply new drawable
|
|
||||||
- `dialog_countdown.xml`:
|
|
||||||
- `btnStop`: change from `Button` to `com.google.android.material.button.MaterialButton`, set square dimensions, apply drawable
|
|
||||||
- `btnRestart`: same treatment
|
|
||||||
- `btnDismiss`: same treatment, keep as gone by default
|
|
||||||
- Adjust the ConstraintLayout chain to accommodate square buttons (may need a horizontal chain with proper spacing)
|
|
||||||
|
|
||||||
### Code changes needed:
|
|
||||||
- `CountdownDialog.kt`: Set button backgrounds programmatically if needed, ensure clickable/pressable states work
|
|
||||||
- No changes to button click logic — only visual overhaul
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
- Use only Material 3 components already in the dependency (`com.google.android.material:material:1.11.0`)
|
|
||||||
- Traditional XML layouts — no Compose
|
|
||||||
- Minimum SDK 24 — no API 30+ features
|
|
||||||
- Keep the app's existing green aesthetic but make it bolder
|
|
||||||
- Maintain accessibility (minimum 48dp touch target, sufficient contrast)
|
|
||||||
Reference in New Issue
Block a user