-
Notifications
You must be signed in to change notification settings - Fork 51
feat(notification): Live Updates focus notifications for active tasks #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- scoped conventions ---'
find /tmp/coderabbit-repo-knowledge/zhou-shilin-aether-3cc49f57 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- target file ---'
sed -n '1,180p' app/src/main/java/com/zhousl/aether/AetherNotificationController.kt
printf '%s\n' '--- relevant usages and project metadata ---'
rg -n -C 3 'Live.?Update|POST_PROMOTED_NOTIFICATIONS|promot|API 35|API 36|Build.VERSION' app/src/main/java app/src/test test 2>/dev/null || true
rg -n -C 2 'compileSdk|minSdk|targetSdk|androidx.core|notification' build.gradle* app/build.gradle* gradle/libs.versions.toml 2>/dev/null || trueRepository: Zhou-Shilin/Aether Length of output: 25652 🌐 Web query:
💡 Result: To support Live Updates in Android 16 (API level 36), developers must declare the android.permission.POST_PROMOTED_NOTIFICATIONS permission in their Android Manifest [1][2][3]. This permission is required to post promoted notifications [1][4]. In your code, you can request promoted treatment for a notification using the NotificationCompat.Builder.setRequestPromotedOngoing(boolean) method, which was introduced in AndroidX Core 1.17.0 [5][6]. Key implementation details for Live Update notifications include: 1. Permission: Add to your AndroidManifest.xml [2][3]. 2. Notification Requirements: The notification must be ongoing (using setOngoing(true)), have a content title, and use a supported style (Standard, BigTextStyle, CallStyle, ProgressStyle, or MetricStyle) [2][3]. 3. Promotion Request: Use.setRequestPromotedOngoing(true) on the NotificationCompat.Builder [2][5][3]. 4. Validation: You can use NotificationManager.canPostPromotedNotifications to check if the app is permitted to post promoted notifications (e.g., based on user settings) [2][7][3]. The NotificationCompat.hasPromotableCharacteristics method can also be used to validate if the notification meets the requirements for promotion [2][3][8]. These APIs allow apps to utilize promoted notification slots for real-time, ongoing updates [2][3]. Citations:
🏁 Script executed: printf '%s\n' '--- manifest permissions and notification declarations ---'
fd -i 'AndroidManifest.xml' .
xargs -r -n1 sh -c 'echo "--- $0"; rg -n -C 2 "permission|service|POST_PROMOTED|notification" "$0"' < <(fd -i 'AndroidManifest.xml' .)
printf '%s\n' '--- notification posting flow and dependency version ---'
sed -n '80,155p' app/src/main/java/com/zhousl/aether/AetherForegroundService.kt
sed -n '185,240p' app/build.gradle.kts
sed -n '1,40p' gradle/libs.versions.toml
sed -n '135,215p' app/src/main/java/com/zhousl/aether/AetherNotificationController.ktRepository: Zhou-Shilin/Aether Length of output: 7271 🏁 Script executed: sed -n '1,180p' app/src/main/AndroidManifest.xml
printf '%s\n' '--- all app permission declarations ---'
rg -n -C 1 '<uses-permission|POST_PROMOTED_NOTIFICATIONS|POST_NOTIFICATIONS' app/src/main/AndroidManifest.xml shared/src/androidMain/AndroidManifest.xmlRepository: Zhou-Shilin/Aether Length of output: 5528 Correct the Live Updates version claim. Change the KDoc and inline comment to Android 16/API 36. On Android 15, this branch can only provide the standard ongoing foreground notification, so document that fallback in the acceptance criteria. 🤖 Prompt for AI AgentsSource: MCP tools |
||
| * 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<ChatSession>, | ||
| executionStates: Map<String, SessionExecutionState>, | ||
|
|
@@ -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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: Zhou-Shilin/Aether
Length of output: 4619
🏁 Script executed:
Repository: Zhou-Shilin/Aether
Length of output: 28710
🌐 Web query:
Android Service stopForeground STOP_FOREGROUND_REMOVE startForeground notification official documentation foreground service💡 Result:
To manage the foreground state and notification associated with an Android Service, you should use the
stopForeground(int)method [1][2]. ThestopForeground(int)method replaces the older, deprecatedstopForeground(boolean)method [1][3]. When calling this, you must pass one of the following integer flags to define the behavior of the service's notification [1][2][3]: 1. STOP_FOREGROUND_REMOVE: This flag removes the service from the foreground state and immediately cancels (removes) the associated notification from the display [1][3]. 2. STOP_FOREGROUND_DETACH: This flag removes the service from the foreground state but leaves the notification visible to the user. The service's association with the notification is severed, meaning the notification will remain even if the service is later stopped or destroyed [1][3]. If you pass zero (0) tostopForeground(int), the system uses a legacy behavior equivalent toSTOP_FOREGROUND_LEGACY(which typically behaves like passing false to the deprecated boolean method) [2][3]. For consistent behavior across different Android versions, it is recommended to useServiceCompat.stopForeground(Service, int)from the AndroidX Core library [4]. This provides a stable interface to manage these flags [4]. When starting a foreground service, you must usestartForeground(int, Notification)or the AndroidX equivalentServiceCompat.startForeground(...)[5]. Note that starting with Android 14 (API level 34), you are required to declare foreground service types in your manifest and pass those types to thestartForegroundcall [5][6][7]. Failure to specify these types when required will result in a runtime exception [5][7].Citations:
Reset
hasEnteredForegroundwhen removing foreground state.stopForeground(STOP_FOREGROUND_REMOVE)removes foreground state, but the flag remainstrue. If active work returns beforeonDestroy,onStartCommandskipsenterForeground, andupdateForegroundonly callsNotificationManagerCompat.notify. SethasEnteredForeground = falsein the stop branch so the service re-enters foreground mode.🤖 Prompt for AI Agents
Source: MCP tools