diff --git a/app/build.gradle b/app/build.gradle
index c7be2a78..fbcaddfb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,9 +8,9 @@ android {
defaultConfig {
applicationId "com.github.muellerma.coffee"
minSdkVersion 21
- targetSdkVersion 34
- versionCode 45
- versionName "2.25"
+ targetSdkVersion 36
+ versionCode 46
+ versionName "2.26"
}
namespace = "com.github.muellerma.coffee"
@@ -55,7 +55,7 @@ aboutLibraries {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- implementation "androidx.core:core-ktx:1.16.0"
+ implementation "androidx.core:core-ktx:1.17.0"
implementation "androidx.appcompat:appcompat:1.7.1"
implementation "androidx.fragment:fragment-ktx:1.8.8"
implementation "com.google.android.material:material:1.12.0"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 65b489a6..a9694428 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
tools:ignore="ProtectedPermissions" />
+
= 36
+ && prefs.useProgressNotifications
+ && ProgressNotificationManager.isAvailable()) {
+ return getProgressNotification(prefs)
+ }
+
+ // Fall back to classic notification style
val stopIntent = Intent(this, ForegroundService::class.java).apply {
action = ACTION_STOP
}
@@ -217,6 +225,22 @@ class ForegroundService : Service(), ServiceStatusObserver {
.build()
}
+ @androidx.annotation.RequiresApi(36)
+ private fun getProgressNotification(prefs: Prefs): Notification? {
+ val status = coffeeApp().lastStatusUpdate
+ if (status is ServiceStatus.Stopped) {
+ return null
+ }
+
+ val remaining = (status as? ServiceStatus.Running)?.remaining
+ val progressManager = ProgressNotificationManager(this)
+ return progressManager.createProgressNotification(
+ remaining = remaining,
+ timeout = prefs.timeout,
+ prefs = prefs
+ )
+ }
+
private fun getTimeoutAction(): NotificationCompat.Action {
Log.d(TAG, "getTimeoutAction()")
val intent = Intent(this, ForegroundService::class.java).apply {
@@ -305,7 +329,7 @@ class ForegroundService : Service(), ServiceStatusObserver {
private val TAG = ForegroundService::class.java.simpleName
private const val ACTION_STOP = "stop_action"
const val ACTION_CHANGE_PREF_TIMEOUT = "change_pref_timeout"
- private const val ACTION_CHANGE_PREF_ALLOW_DIMMING = "change_pref_dimming"
+ const val ACTION_CHANGE_PREF_ALLOW_DIMMING = "change_pref_dimming"
const val NOTIFICATION_ID = 1
const val NOTIFICATION_CHANNEL_ID = "foreground_service"
diff --git a/app/src/main/java/com/github/muellerma/coffee/Prefs.kt b/app/src/main/java/com/github/muellerma/coffee/Prefs.kt
index f1d488ab..df71f126 100644
--- a/app/src/main/java/com/github/muellerma/coffee/Prefs.kt
+++ b/app/src/main/java/com/github/muellerma/coffee/Prefs.kt
@@ -37,4 +37,7 @@ class Prefs(private val context: Context) {
var alternateModeOldTimeout: Int
get() = sharedPrefs.getInt("alternate_mode_old_timeout", -1)
set(value) = sharedPrefs.edit { putInt("alternate_mode_old_timeout", value) }
+
+ val useProgressNotifications: Boolean
+ get() = sharedPrefs.getBoolean("use_progress_notifications", true)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/muellerma/coffee/ProgressNotificationManager.kt b/app/src/main/java/com/github/muellerma/coffee/ProgressNotificationManager.kt
new file mode 100644
index 00000000..61fb168e
--- /dev/null
+++ b/app/src/main/java/com/github/muellerma/coffee/ProgressNotificationManager.kt
@@ -0,0 +1,207 @@
+package com.github.muellerma.coffee
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import kotlin.time.Duration
+
+// Android 16 API level constant (Baklava)
+private const val ANDROID_16_BAKLAVA = 36
+
+/**
+ * Manages Android 16+ Progress-centric notifications for Coffee app.
+ * This creates live activity-style notifications showing wake-lock status and timer progress.
+ */
+@RequiresApi(ANDROID_16_BAKLAVA)
+class ProgressNotificationManager(private val context: Context) {
+
+ /**
+ * Creates an Android 16 progress-centric notification showing Coffee's wake-lock status
+ */
+ fun createProgressNotification(
+ remaining: Duration?,
+ timeout: Int,
+ prefs: Prefs
+ ): Notification {
+ Log.d(TAG, "Creating Android 16 progress-centric notification")
+
+ val stopIntent = Intent(context, ForegroundService::class.java).apply {
+ action = "stop_action"
+ }
+
+ val stopPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.getForegroundService(
+ context,
+ 0,
+ stopIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ } else {
+ PendingIntent.getService(
+ context,
+ 0,
+ stopIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ // Build the progress style notification
+ val progressStyle = NotificationCompat.ProgressStyle()
+
+ // Define the journey for the wake-lock session
+ when {
+ remaining == null -> {
+ // No timeout - indeterminate progress
+ progressStyle.setProgressIndeterminate(true)
+ // Add a single segment for visual styling
+ progressStyle.setProgressSegments(
+ listOf(
+ NotificationCompat.ProgressStyle.Segment(100)
+ .setColor(ContextCompat.getColor(context, R.color.coffee_brown))
+ )
+ )
+ }
+ else -> {
+ // With timeout - show progress through the timer
+ val totalSeconds = timeout * 60
+ val elapsedSeconds = (totalSeconds - remaining.inWholeSeconds.toInt()).coerceAtLeast(0)
+
+ // Add progress segment
+ progressStyle.setProgressSegments(
+ listOf(
+ NotificationCompat.ProgressStyle.Segment(totalSeconds)
+ .setColor(ContextCompat.getColor(context, R.color.coffee_brown))
+ )
+ )
+
+ // Set current progress
+ progressStyle.setProgress(elapsedSeconds)
+ }
+ }
+
+ // Determine notification title based on remaining time
+ val title = if (remaining == null) {
+ context.getString(R.string.notification_title_no_timeout)
+ } else {
+ context.getString(R.string.notification_title_timeout, remaining.toFormattedTime())
+ }
+
+ // Status chip configuration - shows coffee icon and time remaining
+ val whenTime = if (remaining != null) {
+ // Set when time to the end of the countdown (now + remaining time)
+ System.currentTimeMillis() + remaining.inWholeMilliseconds
+ } else {
+ // For no-timeout mode, don't show time in status chip
+ 0L
+ }
+
+ // Short critical text for status chip (7 characters max suggested)
+ val chipText = if (remaining != null && remaining.inWholeMinutes > 0) {
+ "${remaining.inWholeMinutes}min" // Shows remaining minutes in status chip
+ } else {
+ null // No text for indeterminate mode or when < 1 minute
+ }
+
+ // Build the notification with progress style
+ val builder = NotificationCompat.Builder(context, ForegroundService.NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_twotone_free_breakfast_24_accent) // Coffee icon appears in status chip
+ .setContentTitle(title)
+ .setContentText(context.getString(R.string.tap_to_turn_off))
+ .setOngoing(true)
+ .setRequestPromotedOngoing(true) // Request promotion for Live Update - required for status chip
+ .setShowWhen(remaining != null) // Only show time if we have a timeout
+ .setWhen(whenTime) // Countdown end time for status chip
+ .setUsesChronometer(remaining != null) // Use chronometer for countdown timer
+ .setChronometerCountDown(remaining != null) // Count down to zero
+ .setShortCriticalText(chipText) // Shows in status chip (e.g., "5min")
+ .setColor(ContextCompat.getColor(context, R.color.coffee_brown))
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setContentIntent(stopPendingIntent)
+ .setStyle(progressStyle)
+
+ // Add action buttons
+ addActionButtons(builder, prefs)
+
+ return builder.build()
+ }
+
+ private fun addActionButtons(builder: NotificationCompat.Builder, prefs: Prefs) {
+ // Add timeout change action
+ val timeoutIntent = Intent(context, ForegroundService::class.java).apply {
+ action = ForegroundService.ACTION_CHANGE_PREF_TIMEOUT
+ }
+ val timeoutPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.getForegroundService(
+ context,
+ 1,
+ timeoutIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ } else {
+ PendingIntent.getService(
+ context,
+ 1,
+ timeoutIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ val timeoutAction = NotificationCompat.Action(
+ R.drawable.ic_baseline_access_time_24,
+ context.getString(R.string.timeout_next),
+ timeoutPendingIntent
+ )
+
+ // Add dimming toggle action
+ val dimmingIntent = Intent(context, ForegroundService::class.java).apply {
+ action = ForegroundService.ACTION_CHANGE_PREF_ALLOW_DIMMING
+ }
+ val dimmingPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.getForegroundService(
+ context,
+ 2,
+ dimmingIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ } else {
+ PendingIntent.getService(
+ context,
+ 2,
+ dimmingIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ val dimmingTitle = if (prefs.allowDimming) {
+ R.string.allow_dimming_disable
+ } else {
+ R.string.allow_dimming_enable
+ }
+
+ val dimmingAction = NotificationCompat.Action(
+ R.drawable.ic_baseline_brightness_medium_24,
+ context.getString(dimmingTitle),
+ dimmingPendingIntent
+ )
+
+ builder.addAction(timeoutAction)
+ builder.addAction(dimmingAction)
+ }
+
+ companion object {
+ private val TAG = ProgressNotificationManager::class.java.simpleName
+
+ /**
+ * Check if progress-centric notifications are available (Android 16+)
+ */
+ fun isAvailable(): Boolean {
+ return Build.VERSION.SDK_INT >= ANDROID_16_BAKLAVA
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/muellerma/coffee/activities/PreferenceActivity.kt b/app/src/main/java/com/github/muellerma/coffee/activities/PreferenceActivity.kt
index 14226b07..3e0387ad 100644
--- a/app/src/main/java/com/github/muellerma/coffee/activities/PreferenceActivity.kt
+++ b/app/src/main/java/com/github/muellerma/coffee/activities/PreferenceActivity.kt
@@ -67,6 +67,12 @@ class PreferenceActivity : AppCompatActivity() {
return@setOnPreferenceChangeListener true
}
+ // Hide progress notifications preference on pre-Android 16 devices
+ val progressNotificationsPref = preferenceManager.findPreference("use_progress_notifications")
+ if (Build.VERSION.SDK_INT < 36) {
+ progressNotificationsPref?.isVisible = false
+ }
+
val aboutPref = getPreference("about")
aboutPref.setOnPreferenceClickListener {
val fragment = LibsBuilder()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eab6a75c..ce5dedee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -47,6 +47,10 @@
Alternate mode requires this permission
Coffee couldn\'t set the previous timeout
+
+ Use live activity notifications
+ Show rich progress notifications on Android 16+
+
About Coffee
No browser found
diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml
index a2e2911f..bdcb4eef 100644
--- a/app/src/main/res/xml/pref_main.xml
+++ b/app/src/main/res/xml/pref_main.xml
@@ -22,6 +22,12 @@
android:summary="@string/alternate_mode_summary"
android:defaultValue="false"
android:widgetLayout="@layout/preference_material_switch" />
+