Compare commits

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

- Create CountdownService with notification, state persistence,
  and ToneGenerator/Vibrator ownership
- Persist timer state to SharedPreferences for process death recovery
- Add notification with progress bar and Stop/Restart actions
- Embed countdown UI in activity_main.xml alongside picker UI
- Delete CountdownDialog and dialog_countdown.xml
- Update AndroidManifest with foreground service permissions
2026-06-23 01:49:11 +00:00
kfj001 da29e51993 stable, alpine based devcontainer 2026-06-22 23:20:44 +00:00
kfj001 7a91ecba18 visual polish: add pulsing start button and color-shifting progress ring
- Countdown ring interpolates green → yellow → red as timer progresses
- Start button pulses continuously to draw attention
- Pulse stops while dialog is shown and restarts on resume
2026-05-30 10:51:36 -07:00
12 changed files with 778 additions and 445 deletions
+26 -24
View File
@@ -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
# 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"
# ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
# ARG ANDROID_SDK_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
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
# RUN apk update && apk add curl zip git openjdk21-jdk cmake gcc
# 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}
# 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
# 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
# # 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}
# Grant execute permission to gradlew
RUN chmod +x /workspaces/*/gradlew 2>/dev/null || true
# # 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
+4 -1
View File
@@ -7,7 +7,10 @@
"redhat.vscode-xml",
"redhat.vscode-yaml",
"felipecaslazana.vscode-android-logcat",
"ms-azuretools.vscode-docker"
"ms-azuretools.vscode-docker",
"fwcd.kotlin",
"vscjava.vscode-java-pack",
"Anthropic.claude-code"
]
}
},
+11 -8
View File
@@ -17,16 +17,19 @@
- **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/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.
- `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/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.
+7
View File
@@ -1,6 +1,8 @@
<?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"
@@ -16,5 +18,10 @@
<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>
@@ -47,6 +47,13 @@ class CountdownCanvasView @JvmOverloads constructor(
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
@@ -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,8 +1,12 @@
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
@@ -11,35 +15,52 @@ 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() {
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 = 10000L
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) {
5000L -> R.id.chip5s
15000L -> R.id.chip15s
30000L -> R.id.chip30s
else -> R.id.chip10s
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.chip5s -> 5000L
R.id.chip10s -> 10000L
R.id.chip15s -> 15000L
R.id.chip30s -> 30000L
R.id.chip5m -> 300000L
R.id.chip10m -> 600000L
R.id.chip15m -> 900000L
R.id.chip30m -> 1800000L
else -> DEFAULT_DURATION
}
@@ -49,7 +70,6 @@ class MainActivity : AppCompatActivity() {
private fun onChipClicked(chipId: Int) {
if (chipId == lastSelectedChipId) {
// Re-selection: provide feedback so user knows the selection is registered
vibrator.vibrate(VibrationEffect.createOneShot(15, VibrationEffect.DEFAULT_AMPLITUDE))
val originalText = btnDurationLabel.text
btnDurationLabel.text = "${selectedDuration / 1000}s!"
@@ -57,6 +77,89 @@ class MainActivity : AppCompatActivity() {
}
}
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)
@@ -64,12 +167,22 @@ class MainActivity : AppCompatActivity() {
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
@@ -93,25 +206,66 @@ class MainActivity : AppCompatActivity() {
val checkedId = if (checkedIds.size > 0) checkedIds[0].toInt() else -1
if (checkedId != -1) {
if (checkedId != lastSelectedChipId) {
// New selection: update duration, save, and refresh label
selectedDuration = chipIdToDuration(checkedId)
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
updateDurationLabel()
lastSelectedChipId = checkedId
}
// Always close the panel on any selection
hsvDurationOptions.visibility = View.GONE
}
}
// Set individual click listeners to detect re-selection
findViewById<View>(R.id.chip5s).setOnClickListener { onChipClicked(R.id.chip5s) }
findViewById<View>(R.id.chip10s).setOnClickListener { onChipClicked(R.id.chip10s) }
findViewById<View>(R.id.chip15s).setOnClickListener { onChipClicked(R.id.chip15s) }
findViewById<View>(R.id.chip30s).setOnClickListener { onChipClicked(R.id.chip30s) }
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) }
findViewById<View>(R.id.btnStart).setOnClickListener {
CountdownDialog(this, selectedDuration, prefs).show()
// 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()
}
+274 -191
View File
@@ -29,221 +29,304 @@
android:layout_height="wrap_content"
android:padding="24dp">
<ImageView
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"
<!-- Picker UI -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pickerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/instruction_title"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/instructionsLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvInstructionTitle">
<ImageView
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" />
<LinearLayout
<TextView
android:id="@+id/tvInstructionTitle"
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_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>
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/instruction_title"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timerIcon" />
<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:id="@+id/instructionsLayout"
android:layout_width="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
android:id="@+id/chip5s"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
<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_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_height="wrap_content"
android:minWidth="56dp"
android:text="5s"
app:chipBackgroundColor="@color/chip_duration_bg"
app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp"
android:textColor="@color/chip_duration_text" />
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip10s"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="56dp"
android:text="10s"
app:chipBackgroundColor="@color/chip_duration_bg"
app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp"
android:textColor="@color/chip_duration_text" />
<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/chip15s"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="56dp"
android:text="15s"
app:chipBackgroundColor="@color/chip_duration_bg"
app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp"
android:textColor="@color/chip_duration_text" />
<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/chip30s"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="56dp"
android:text="30s"
app:chipBackgroundColor="@color/chip_duration_bg"
app:chipStrokeColor="@color/chip_duration_stroke"
app:chipStrokeWidth="2dp"
android:textColor="@color/chip_duration_text" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<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" />
<View
android:id="@+id/spacerBelowChips"
android:layout_width="1dp"
android:layout_height="5dp"
android:layout_marginTop="8dp"
<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_toBottomOf="@+id/hsvDurationOptions" />
app:layout_constraintTop_toTopOf="parent">
<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">
<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" />
<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" />
<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>
<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>
@@ -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>
+4 -4
View File
@@ -1,12 +1,12 @@
<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">Light your bowl</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 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="settings">Settings</string>
<string name="vibration">Vibration</string>