From 4a5ae0ffff3511c31ede1cfc48bd6cf96a19a3b0 Mon Sep 17 00:00:00 2001 From: X-SCI-TECH Date: Sun, 30 Aug 2026 18:43:08 +0000 Subject: [PATCH] feat(notification): add Live Updates focus notifications for active tasks Implement Android 15+ Live Updates (promoted ongoing foreground notifications) via the AndroidX compat path: - Add POST_PROMOTED_NOTIFICATIONS permission - New LiveUpdateChannelId channel for active-task notifications - buildForegroundNotification uses LiveUpdateChannelId + setRequestPromotedOngoing(true) while sessions are active, with a live status line (task name + running tool) - AetherForegroundService.updateForeground() refreshes the ongoing notification in place (same ID) on every status change - Bump androidx.core (core-ktx) 1.13.1 -> 1.17.0 for setRequestPromotedOngoing --- app/src/main/AndroidManifest.xml | 1 + .../zhousl/aether/AetherForegroundService.kt | 17 +++++- .../aether/AetherNotificationController.kt | 56 +++++++++++++++++-- gradle/libs.versions.toml | 2 +- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b9c2888d..f49cf835 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/app/src/main/java/com/zhousl/aether/AetherForegroundService.kt b/app/src/main/java/com/zhousl/aether/AetherForegroundService.kt index 09899836..3b37824d 100644 --- a/app/src/main/java/com/zhousl/aether/AetherForegroundService.kt +++ b/app/src/main/java/com/zhousl/aether/AetherForegroundService.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.IBinder +import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import java.util.concurrent.atomic.AtomicBoolean @@ -43,7 +44,7 @@ class AetherForegroundService : Service() { stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } else { - enterForeground( + updateForeground( runtime.notificationController.buildForegroundNotification( sessions = sessions, executionStates = executionStates, @@ -90,6 +91,20 @@ class AetherForegroundService : Service() { hasEnteredForeground = true } + /** + * Updates the Live Update foreground notification in place (same ID), so the + * system keeps the promoted-ongoing behavior and just refreshes the content + * — this is what makes the task progress "live". + */ + private fun updateForeground(notification: android.app.Notification) { + val notificationManager = NotificationManagerCompat.from(this) + if (hasEnteredForeground) { + notificationManager.notify(ForegroundNotificationId, notification) + } else { + enterForeground(notification) + } + } + companion object { private val startRequested = AtomicBoolean(false) diff --git a/app/src/main/java/com/zhousl/aether/AetherNotificationController.kt b/app/src/main/java/com/zhousl/aether/AetherNotificationController.kt index ebe8906e..21ca74b6 100644 --- a/app/src/main/java/com/zhousl/aether/AetherNotificationController.kt +++ b/app/src/main/java/com/zhousl/aether/AetherNotificationController.kt @@ -17,6 +17,7 @@ import com.zhousl.aether.ui.ChatSession private const val ForegroundChannelId = "aether_background_runs" private const val CompletionChannelId = "aether_completed_runs" +private const val LiveUpdateChannelId = "aether_live_updates" const val ForegroundNotificationId = 1001 class AetherNotificationController( @@ -43,10 +44,32 @@ class AetherNotificationController( ).apply { description = "Alerts you when a background Aether session finishes." } + val liveUpdateChannel = NotificationChannel( + LiveUpdateChannelId, + "Live task progress", + NotificationManager.IMPORTANCE_LOW, + ).apply { + description = "Shows real-time progress of Aether tasks while the app is in the background." + setShowBadge(false) + } manager.createNotificationChannel(foregroundChannel) manager.createNotificationChannel(completionChannel) + manager.createNotificationChannel(liveUpdateChannel) } + /** + * Builds the foreground notification for active background sessions. + * + * On Android 15+ (API 35+) the notification is promoted to a **Live Update** + * (a.k.a. focus notification): while the app is in the foreground the + * notification stays quiet, and when the app loses focus (goes to the + * background) the system surfaces it more prominently — status-bar chip, + * lock screen and top of the notification drawer — with the latest task + * progress, without the user opening the app. + * + * On older devices this gracefully degrades to a standard ongoing + * foreground notification. + */ fun buildForegroundNotification( sessions: List, executionStates: Map, @@ -71,23 +94,46 @@ class AetherNotificationController( PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentMutabilityFlags(), ) - return NotificationCompat.Builder(context, ForegroundChannelId) + // Live status detail: the most relevant running tool/status for each active session. + val liveStatusLines = activeSessions.mapNotNull { session -> + val state = executionStates[session.id] ?: return@mapNotNull null + val detail = state.pendingStatusDetail.ifBlank { state.pendingStatusText } + val tool = state.pendingToolInvocations.firstOrNull { it.isRunning }?.toolName + val suffix = when { + tool != null -> " · $tool" + detail.isNotBlank() -> " · $detail" + else -> "" + } + (session.title.ifBlank { "Untitled chat" } + suffix).takeIf { it.isNotBlank() } + } + + val builder = NotificationCompat.Builder( + context, + if (activeSessions.isNotEmpty()) LiveUpdateChannelId else ForegroundChannelId, + ) .setSmallIcon(R.drawable.ic_notification_small) .setContentTitle(title) .setContentText(body) .setStyle( NotificationCompat.BigTextStyle() .bigText( - activeSessions.joinToString(separator = "\n") { session -> - "- ${session.title.ifBlank { "Untitled chat" }}" - }.ifBlank { body } + liveStatusLines.joinToString("\n").ifBlank { body } ) ) .setOngoing(true) .setOnlyAlertOnce(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setContentIntent(contentIntent) - .build() + + if (activeSessions.isNotEmpty()) { + // Live Update / focus notification (Android 15+): request promoted-ongoing + // rendering so the task progress is elevated while the app is backgrounded. + // AndroidX Core 1.17 maps this to EXTRA_REQUEST_PROMOTED_ONGOING on the + // platform notification, which is the supported compat path for Live Updates. + builder.setRequestPromotedOngoing(true) + } + + return builder.build() } fun notifyCompletion( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7abecb9f..a444d74f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.13.2" kotlin = "2.4.0" -coreKtx = "1.13.1" +coreKtx = "1.17.0" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.2" appcompat = "1.7.0"