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" /> +