Compare commits
4
Commits
97b01437b2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c822484d | ||
|
|
d7ce314c22 | ||
|
|
da29e51993 | ||
|
|
7a91ecba18 |
+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
|
||||
# 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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user